【问题标题】:FtpWebRequest 30 minute time outFtpWebRequest 30 分钟超时
【发布时间】:2023-06-10 15:18:01
【问题描述】:

通过 FTP 下载大文件时,我的代码在 30 分钟后出现超时异常。服务器是在 Windows 上运行的 FileZilla。我们有一个 SSL 证书,配置了选项 Enable FTP over SSL/TLS support (FTPS)Allow explicit FTP over TLS 启用。我可以访问服务器和 FileZilla 配置,但看不到任何可能导致此行为的内容。下面是在 Windows 2012 服务器机器上的 .NET 4.6.2 上运行的源代码。它可以从 FTP 服务器下载文件,但如果文件下载时间超过 30 分钟,则会在恰好 30 分钟后超时(如下所列)。

作为测试,我使用FileZilla Client,在同一台客户端 PC 上运行,同时从同一服务器端点下载多个大文件,这样每个文件的下载需要 30 多分钟才能完成。在这种情况下没有发生错误。

我在*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

FileZilla - 常规设置

 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)

FileZilla 服务器日志

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"


进度更新:

  • (2018.02.20) 我要求我们的网络团队检查防火墙规则,但他们找不到任何感兴趣的内容。
  • (2018.02.22) 我发现正在使用的 FileZilla Server 版本是0.9.43 beta根据更改日志发布日期为 2014-01-02)。尽管我在更改日志中没有发现任何表明此行为是已修复的错误的信息,但我将升级到最新版本 0.9.60.2发布日期 2017-02-08)并执行再次测试。将在 24 小时内报告。
  • (2018.02.23) FileZilla 已更新至最新版本。这并没有解决问题,我更新了服务器日志,但它看起来与之前的日志几乎相同,只是最后一次传输是通过 TLS 而不是 SSL 进行的。
  • (2018.02.23) 我在 FileZilla 支持页面上找到了以下链接 Timeouts on large files。我将把它推回给我们托管服务提供商的网络工作人员再看看。
  • (2018.02.27) 原来是公司防火墙(客户端执行的网络)而不是托管防火墙(FileZilla 所在的网络)是罪魁祸首。它被配置为在1800 秒后丢弃空闲连接。添加了一条规则来覆盖这两个端点之间的内容。

罪魁祸首/答案

事实证明,罪魁祸首是公司防火墙(客户端执行的网络)而不是托管防火墙(托管 FileZilla 的网络)。它被配置为在1800 秒后丢弃空闲连接。添加了一条规则来覆盖这两个端点之间的内容。

【问题讨论】:

  • 你检查防火墙和网络设置了吗? Fillezilla 不应该在奇怪的转移过程中中断......
  • @SteliosAdamantidis - 感谢您的建议,很遗憾这并没有改变结果。
  • 您确定文件没有以任何方式损坏吗?可以用ftp软件下载吗,如果可以,可以打开下载的副本吗?
  • @FrancescoB。 - 将KeepAlive 设置为true,将TimeOut 设置为-1,在无限中不会改变结果。不知道我还应该在代码中更改什么。我没有尝试过每 xx 分钟重用一次请求。此外,此服务器不在负载平衡器后面,但不在网络中。我已要求网络管理员检查防火墙,但他们无法提出任何建议。使用 FileZilla 客户端进行了测试以查看是否也失败了,那么原因与我的代码无关,但是通过该工作,我感觉它可能是特定于代码的。
  • 关于您的最新更新,我也找到了该文档,并且正在根据该文档准备答案。令我困扰的是,使用另一个 FTP 客户端,你成功了。我建议您使用网络工具(wireshark 等)来查看 FileZilla 与您的 .NET 应用程序的不同之处。

标签: c# .net ftp filezilla ftpwebrequest


【解决方案1】:

也许您应该尝试另一种 FTP 协议客户端的实现,它不是基于 FtpWebRequest 构建的。

相关问题由来已久,没有明确的解决方案或答案。所以我会尝试像FluentFTP 这样的东西,它直接使用Winsock API。 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 的推荐,与具有更多功能的内置FtpWebRequest 相比,它看起来是一个更易于使用的库。最终原因是防火墙问题,但您也可以争辩说,使用适当的库,一开始就不是问题。无论哪种方式,我相信这个答案最能解决这个问题。谢谢。
  • 请问防火墙问题是什么?无论如何知道会很有趣...谢谢:)
  • @FrancescoB。 - 另请参阅底部列出的原因更新的问题。
【解决方案2】:

是的,我认为您的代码中没有“错误”;只是控制连接在 30 分钟后断开,即使传输连接没有超时。也许甚至没有必要更改您的 KeepAlive 和 Timeout 值,只需尝试每隔 20 分钟左右通过虚拟下载重用您的请求:这样您将重置控制连接计时器。

顺便说一句,我在某处读到 30 分钟是 FileZilla Server 的标准超时,它基于配置为每 300 秒发送一次的 6 个保持活动状态(这为您提供 30 分钟的体验)。 如果您可以尝试使用另一台 FTP/FTPS 服务器,您可能会发现不同的空闲超时,并且不会违反 30 分钟的限制(但会有所不同)。

所以,我个人会投资于编写async 下面的代码,因此执行流程在封闭的using 之后继续,您可以进入一个循环,每 20 分钟重复一次您的请求(及其控制连接)进行虚拟下载。 当然,FileZilla Client 不需要虚拟下载,因为它在较低级别上运行,并且可能会发送 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);
            }
        }

【讨论】:

  • 如果您不想让代码异步,您可以将request 传递给另一个线程并让它保持连接状态(即下载一个小文件,例如一个 1 字节大小的文件) .
  • 某处我读到 30 分钟是 FileZilla 服务器的标准超时时间 最好找到 somewhere,因为我已经搜索过了,并且我什么也没找到。另外,如果在某个时候 30 变为 15 怎么办?我不喜欢every 20 minutes 位。更好的解决方案是找到有问题的软件,而不是试图绕过它。
  • @SteliosAdamantidis 关于来源你是对的,不幸的是我在发布之前(或现在)找不到它;一个link 以一种相当间接的方式陈述了同样的事情。你也是对的,20 分钟的例子是……一个例子,基于当前的服务器行为。显然,一般来说,如果任何服务器更改其行为(例如,在策略、超时或包含的证书方面),则应通知客户端。
【解决方案3】:

有一个类似的issue reported in FluentFTP herestefanolazzarato posted a work-around

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

【讨论】: