ファイルをダウンロードするスタンドアロンFTPクライアント

スタンドアロンftpクライアントの開発での経験を共有したいと思います。



データがグラフィックイメージとテキストファイルの形式で定期的に表示されるFTPサーバーがあり、そのサイズは数十キロバイトから数ギガバイトまでさまざまです。 インターネットへのアクセスは、有線またはGSMホイッスルを介して、または一般的に衛星を介して(つまり、それぞれ安定および不安定)アクセスできます。 2番目のケースでは、気象条件、地理的位置などにより接続が失われる確率が急激に増加します。



したがって、顧客の要件は次のとおりです。



  1. ftp-serverをポーリングして、新しいファイルとそのダウンロードを確認します。
  2. ダウンロードの突然の停止(切断、またはFTPクライアントがインストールされているシステム)がクラッシュした場合、ダウンロードはできるだけ早く続行する必要があります。
  3. ダウンロード速度を制限します(これはGSMトラフィックのコストによるものです)。


問題を解決する私の方法が興味深い場合は、カットをお願いします!



便宜上、コード例と作業の複雑さのより詳細な説明を使用して、記事全体をクライアントの作業の主要な段階に分けることができます。



問題の声明



考えた後、次のスキームに従って動作するクライアントを作成することにしました。



  1. サーバーをノックし、ファイルのリストを受け取ります。
  2. ダウンロード履歴を調べ、ファイルが履歴にない場合は、ファイルをダウンロードキューに追加します。
  3. 何らかの理由でファイルをダウンロードできなかった場合は、ダウンロードキューの最後に送信されます。
  4. 正常にアップロードされたファイルが履歴に追加されます。


そしていくつかの機能:





サーバーの定期的なポーリングとファイルのリストの取得



サーバーを定期的にポーリングするという決定は、すぐに頭に浮かぶ-ファイルのリストを取得する方法を含むタイマーを開始する。 ただし、サーバーには少し独特なディレクトリ構造があります。 要するに、サーバーには2つのフォルダーがあります-通知とファイルです。 filesフォルダーには、ダウンロードする必要のあるデータ自体が含まれており、それらはすべてタイプFILE_ID_xxxの一意の名前を持っています。xは任意の数字です。 通知フォルダーには、実際の名前、サーバーへの配置日、サイズなど、 ファイルフォルダーからのファイルの説明を含むxmlファイルが含まれます。



通知フォルダーからすべてのxmlを読み取った後、単純なFileItemからコレクションを作成します。



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; }
      
      





次に、コレクションを調べて、ファイルがダウンロード履歴に存在するかどうか、および現在ロード中かどうかを確認します



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





以上です。 サーバーのポーリングと新しいファイルの検索が完了しました。 そのようなFileHistoryクライアントについて-私はさらに書きます。



ファイルを複数のストリームにアップロードする



上記のコードの「 クライアント 」は、サーバーからのファイルのダウンロードのみを処理するFTPClientクラスのインスタンスです。 実際、 FTPClientは私のFtpWebRequestラッパーです。



FTPClientには、「ダウンロードキュー」と呼ばれるスレッドセーフキューがあります。



 private ConcurrentQueue<FileItem> downloadQueue;
      
      





したがって、 DownloadFileメソッドを呼び出すとどうなりますか。



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





ファイルはダウンロードキューに追加され、その後メソッドが呼び出され、TPLを使用してファイルをダウンロードするタスクが作成されます。 これは次のようなものです。



 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); } } }
      
      





ロシア語を話す場合、最初にファイル読み込みタスクが既にいくつ動作しているか、また別のタスクをプッシュする可能性があるかどうかを確認します。 次に、キューが空でない場合、ダウンロードキューからFileItemを取得しようとします。 次に、ファイルがすでにローカルに存在するかどうかを判断します。 ダウンロードが予期せず中断された場合、ファイルがローカルに存在する可能性があります。 ダウンロードに成功したものはすべてディスクに残ります。 したがって、この場合、中断したところからダウンロードを開始します。



OnTaskCompleteメソッド。DownloadTaskの完了後に呼び出されます。



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





つまり、アクティブなダウンロードのカウンターを減らし、新しいダウンロードタスクを開始しようとします。 つまり、新しいファイルがダウンロードキューに追加され、現在のダウンロードタスクが完了すると、新しいダウンロードタスクが作成されることがわかります。



サーバーからファイルを直接ダウンロードするメソッド:



 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); } }
      
      





そして、サーバーへのリクエストを形成し、レスポンスを返すメソッド:



 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(); }
      
      





つまり、最初からではなくストリームの読み取りを開始するには、要求を生成するときに次の行を使用します。



 request.ContentOffset = offset;
      
      





また、速度制限は次のように機能します。まず、 downloadLimit 、現在のストリームがロードできるバイト数を計算しましょう。 一般的な速度制限とアクティブなダウンロードスレッドの数が考慮されます。 次に、1024バイトのストリームを読み取ります。 どれくらい時間がかかったかを見つけました( timePassed )。 読み取られたバイトの総数は、 downloadedBytesに書き込まれます



制限を超えた場合、2番目の終わりまで残りの時間だけ一時停止するようにストリームを設定します。



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





1秒後に、カウンターはゼロにリセットされます。



また、 WebExeptionの場合ファイルは再びダウンロードキューに追加されます。 また、ファイルは正常に完了した後にのみ履歴に記録されます。



ダウンロード履歴



ダウンロード履歴をファイルに保存すると、アプリケーションが突然再起動し、ランタイムによって保存された履歴が失われた場合に役立ちます。



内部では、 FileHistoryクラスにはFileItemを格納するコレクションがあり、既に正常にダウンロードされています。



 private static List<FileItem> downloadHistory;
      
      





ファイルの追加は非常に簡単です。ファイルをコレクションに追加し、すぐに変更をxmlに書き込みます。



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





そして、これは、履歴内のファイルをチェックしたいときに起こることです。



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





説明させてください-検証メソッドが呼び出されます。 コレクションのエントリはゼロです。 どうやら、アプリケーションがクラッシュし、ストーリーが失われたようです。 この場合、xmlから履歴を復元しようとします。 これが失敗した場合(ファイルが見つからないか破損している)-このファイルをまだダウンロードしていないと思われます。



完了



この記事が、私のようにftpクライアントを初めて作成しなければならない人にも役立つことを願っています。 私は、解決策が完璧であるふりをしません。 そして、これがHabrに関する記事を書いた最初の経験なので、私は批判とコメントを受け入れます。



All Articles