FTP経由で大きなファイルをダウンロードすると、ちょうど30分後にコードでタイムアウト例外が発生します。サーバーは FileZilla Windowsで実行されています。オプションEnable FTP over SSL/TLS support (FTPS)
およびAllow explicit FTP over TLS
を有効にしてSSL証明書を構成しました。サーバーと FileZilla 構成にアクセスできますが、この動作を引き起こす可能性のあるものは何も表示されません。以下は、Windows 2012Serverマシンの.NET4.6.2で実行されているソースコードです。 FTPサーバーからファイルをダウンロードできますが、ファイルのダウンロードに30分以上かかる場合は、正確に30分後に例外(以下にリスト)を除いてタイムアウトします。
テストとして、同じクライアントPCから実行している FileZilla Client を使用して、同じサーバーエンドポイントから複数の大きなファイルを同時にダウンロードし、各ファイルのダウンロードが完了するまでに30分以上かかったようにしました。このシナリオではエラーは発生しませんでした。
StackOverflow と Google を検索しましたが、有望なものは何も見つかりませんでした。誰かがどこを見るべきか(サーバー側またはクライアント側)についていくつかのヒントを持っているなら、私は最も感謝するでしょう。
public class FtpFileDownloader
{
// log4net
private static readonly ILog Logger = LogManager.GetLogger(typeof(FtpFileDownloader));
public void DownloadFile()
{
// setting the SecurityProtocol did not change the outcome, both were tried. Originally it was not set at all.
// ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
const int timeout = 7200000;
const string file = "some-existing-file";
try
{
var request = (FtpWebRequest) WebRequest.Create("uri-path-to-file");
request.KeepAlive = false;
request.Timeout = -1;
request.ReadWriteTimeout = timeout;
request.Credentials = new NetworkCredential("userName", "password");
request.UsePassive = true;
request.EnableSsl = true;
request.Method = WebRequestMethods.Ftp.DownloadFile;
Logger.Debug($"Downloading '{file}'");
using (var response = (FtpWebResponse) request.GetResponse())
using (var sourceStream = response.GetResponseStream())
using (var targetStream = new FileStream("some-target-on-disk", FileMode.Create, FileAccess.Write))
{
try
{
sourceStream.CopyTo(targetStream);
targetStream.Flush();
Logger.Debug($"Finished download '{file}'");
}
catch (Exception exInner)
{
Logger.Error($"Error occurred trying to download file '{file}'.", exInner);
}
}
}
catch (Exception ex)
{
Logger.Error($"Error occurred trying to dispose streams when downloading file '{file}'.", ex);
}
}
}
ERROR FtpFileDownloader - Error occurred trying to download file 'some-existing-file'.
System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
at System.Net.FixedSizeReader.ReadPacket(Byte[] buffer, Int32 offset, Int32 count)
at System.Net.Security._SslStream.StartFrameBody(Int32 readBytes, Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security._SslStream.StartFrameHeader(Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security._SslStream.StartReading(Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security._SslStream.ProcessRead(Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.TlsStream.Read(Byte[] buffer, Int32 offset, Int32 size)
at System.Net.FtpDataStream.Read(Byte[] buffer, Int32 offset, Int32 size)
at System.IO.Stream.InternalCopyTo(Stream destination, Int32 bufferSize)
at System.IO.Stream.CopyTo(Stream destination)
at FtpFileDownloader.DownloadFile
ERROR FtpFileDownloader - Error occurred trying to dispose streams when downloading file 'some-existing-file'.
System.Net.WebException: The underlying connection was closed: An unexpected error occurred on a receive.
at System.Net.FtpWebRequest.SyncRequestCallback(Object obj)
at System.Net.FtpWebRequest.RequestCallback(Object obj)
at System.Net.CommandStream.Dispose(Boolean disposing)
at System.IO.Stream.Close()
at System.IO.Stream.Dispose()
at System.Net.ConnectionPool.Destroy(PooledStream pooledStream)
at System.Net.ConnectionPool.PutConnection(PooledStream pooledStream, Object owningObject, Int32 creationTimeout, Boolean canReuse)
at System.Net.FtpWebRequest.FinishRequestStage(RequestStage stage)
at System.Net.FtpWebRequest.SyncRequestCallback(Object obj)
at System.Net.FtpWebRequest.RequestCallback(Object obj)
at System.Net.CommandStream.Abort(Exception e)
at System.Net.CommandStream.CheckContinuePipeline()
at System.Net.FtpWebRequest.DataStreamClosed(CloseExState closeState)
at System.Net.FtpDataStream.System.Net.ICloseEx.CloseEx(CloseExState closeState)
at System.Net.FtpDataStream.Dispose(Boolean disposing)
at System.IO.Stream.Close()
at System.IO.Stream.Dispose()
at FtpFileDownloader.DownloadFile
Listen on these ports: 21
Max. number of users: 0 (infinite)
Number of threads: 2
Connection timeout: 120 (seconds)
No Transfer timeout: 9000 (seconds)
Log timeout: 60 (seconds)
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> Connected on port 21, sending welcome message...
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> 220 Welcome
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> AUTH TLS
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> 234 Using authentication type TLS
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> TLS connection established
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> USER my-user-account
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> 331 Password required for my-user-account
23-2-2018 11:40:40 - (not logged in) (194.123.75.2)> PASS **************
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 230 Logged on
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> PBSZ 0
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 200 PBSZ=0
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> PROT P
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 200 Protection level set to P
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> OPTS utf8 on
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 202 UTF8 mode is always enabled. No need to send this command.
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> PWD
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 257 "/" is current directory.
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> TYPE I
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 200 Type set to I
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> PASV
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 227 Entering Passive Mode (IP-ADDRESS,245,222)
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> RETR path-to-file
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> 150 Opening data channel for file download from server of "/path-to-file"
23-2-2018 11:40:40 - my-user-account (194.123.75.2)> TLS connection for data connection established
23-2-2018 12:10:41 - my-user-account (194.123.75.2)> disconnected.
切断(最後の行)とその前の行の間に30分があることに注意してください。転送が正常に完了した場合は、disconnected
行の前に226 Successfully transferred "/path-to-file"
を読み取る行もあります
進捗状況の更新:
0.9.43 beta
(変更ログによるとリリース日2014-01-02)であることがわかりました。この動作が修正されたバグであることを示唆する変更ログには何も見つかりませんでしたが、最新バージョン0.9.60.2
(リリース日2017-02-08)そしてテストを再実行します。 24時間以内に報告します。1800
秒後にアイドル接続をドロップするように構成されました。これらの2つのエンドポイント間でこれをオーバーライドするルールが追加されました。会社のファイアウォール(クライアントが実行されるネットワーク)ではなく、ホスティングファイアウォール(FileZillaがホストされるネットワーク)であることが判明しました。犯人でした。 1800
秒後にアイドル接続をドロップするように構成されました。これらの2つのエンドポイント間でこれをオーバーライドするルールが追加されました。
おそらく、FtpWebRequest
の上に構築されていないFTPプロトコルクライアントの別の実装を試す必要があります。
関連する問題は長い間存在し、明確な解決策や答えがありません。だから私は FluentFTP のようなものを試してみます、それはWinsockAPIを直接使用します。 XMLドキュメントのコメントでは、DownloadFile()
は大きなファイルのダウンロードを適切に処理する必要があると述べています。
/// <summary>
/// Downloads the specified file onto the local file system.
/// High-level API that takes care of various Edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
詳細については、以下を確認してください。
同様の ここでFluentFTPで報告された問題 および stefanolazzaratoが回避策を投稿しました :
int progress = -1; try { FtpClient client = new FtpClient("Host"); client.Credentials = new NetworkCredential("USER", "PASSWORD"); client.Connect(); client.UploadFile("LOCALPATH/FILENAME", "REMOTEPATH/FILENAME", FtpExists.Overwrite, false, FtpVerify.None, new Progress<FtpProgress>(p => progress = Convert.ToInt32(p.Progress)) ); } catch (Exception ex) { if (progress == 100 && ex is FluentFTP.FtpException && ex.InnerException != null && ex.InnerException is TimeoutException) { // Upload complete // LOG Info exception } else { // LOG Fatal exception throw; } }
はい、コードに「間違い」はないと思います。転送接続がタイムアウトしなくても、30分後に制御接続が切断されるだけです。おそらく、KeepAliveとTimeoutの値を変更する必要はないでしょう。ダミーのダウンロードを使用して、20分ごとにリクエストを再利用してみてください。そうすれば、制御接続タイマーがリセットされます。
ところで、私が読んだところによると、30分はFileZilla Serverの標準のタイムアウトであり、300秒ごとに送信されるように構成された6つのキープアライブに基づいています(30分のエクスペリエンスが得られます)。別のFTP/FTPSサーバーで試すことができれば、おそらく別のアイドルタイムアウトが見つかり、その30分の制限(ただし別の制限)にぶつかることはありません。
したがって、個人的にはasync
の下のコードを作成することに投資します。そのため、実行フローはusing
を囲んだ後に続行され、20分ごとにリクエストを再利用するループに入ることができます(および制御接続)ダミーダウンロードを行います。もちろん、FileZillaクライアントは下位レベルで動作し、おそらく制御接続を維持するためにTCPコマンドを送信するため、ダミーのダウンロードは必要ありません。
using (var response = (FtpWebResponse) request.GetResponse())
using (var sourceStream = response.GetResponseStream())
using (var targetStream = new FileStream("some-target-on-disk", FileMode.Create, FileAccess.Write))
{
try
{
sourceStream.CopyTo(targetStream);
targetStream.Flush();
Logger.Debug($"Finished download '{file}'");
}
catch (Exception exInner)
{
Logger.Error($"Error occurred trying to download file '{file}'.", exInner);
}
}