FtpWebRequest 30-минутный тайм-аут

Мой код испытывает исключение тайм-аута через ровно 30 минут при загрузке большого файла по FTP. Сервер - это FileZilla, работающий в Windows. У нас есть SSL-сертификат, настроенный с параметрами. Enable FTP over SSL/TLS support (FTPS) и Allow explicit FTP over TLS. Я имею доступ к серверу и конфигурации FileZilla, но не вижу ничего, что могло бы вызвать подобное поведение. Ниже приведен исходный код, который выполняется на.NET 4.6.2 на машине Windows 2012 Server. Он может загружать файлы с FTP-сервера, но будет тайм-аут с исключением (указанным ниже) через ровно 30 минут, если для загрузки файла потребуется более 30 минут.

В качестве теста я использовал FileZilla Client, работающий с одного и того же клиентского ПК, для загрузки нескольких больших файлов с одной и той же конечной точки сервера одновременно, чтобы загрузка для каждого файла заняла более 30 минут. В этом сценарии ошибок не было.

Я искал /qaru.site/... а также 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 минут. Если он успешно завершил передачу, также будет строка, которая будет читать 226 Successfully transferred "/path-to-file" до disconnected линии


Обновление прогресса:

  • (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. Я собираюсь вернуть его персоналу сети нашего хостинг-провайдера, чтобы еще раз взглянуть на него.
  • (2018.02.27). Оказалось, что виновником является брандмауэр компании (сеть, где выполняется клиент), а не брандмауэр хостинга (сеть, где размещается FileZilla). Он был настроен на сброс простоя соединений через 1800 секунд. Было добавлено правило, чтобы переопределить это между этими двумя конечными точками.

Culprit/Answer

Оказалось, что виновником является брандмауэр компании (сеть, где выполняется клиент), а не хостинг-брандмауэр (сеть, где размещается FileZilla). Он был настроен на сброс простоя соединений через 1800 секунд. Было добавлено правило, чтобы переопределить это между этими двумя конечными точками.

Ответ 1

Возможно, вам стоит попробовать еще одну реализацию клиента FTP-протокола, который не построен поверх FtpWebRequest.

Связанные с этим проблемы существуют долгое время, у них нет четкого решения или ответа. Поэтому я бы попробовал что-то вроде FluentFTP, он напрямую использует Winsock API. В комментарии XML Documentation Comment сказано, что 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>

Для получения дополнительной информации проверьте:

Ответ 2

Да, я не думаю, что в вашем коде есть "ошибка"; это просто, что контрольное соединение замирает через 30 минут, даже несмотря на то, что соединение передачи не отключается. Возможно, вам даже не придется изменять значения KeepAlive и Timeout, просто попробуйте повторно использовать свой запрос каждые 20 минут или около того с помощью фиктивной загрузки: таким образом вы сбросите таймер соединения управления.

Кстати, где-то я читал, что 30 минут - это стандартный тайм-аут для FileZilla Server, который основан на настройке 6 keep-alive, настроенных для отправки каждые 300 секунд (что дает вам 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);
            }
        }

Ответ 3

Об аналогичной проблеме сообщалось в 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;
   }
}