При работе с файловым обменом через FTP-серверы разработчики часто сталкиваются с крайне неприятной ситуацией: соединение с сервером устанавливается успешно, однако при попытке скачать или отправить файл метод Получить() или Записать() случайным образом выдает ошибку «Ошибка аутентификации при доступе к ресурсу: Access denied to remote resource». Самое загадочное здесь то, что ошибка имеет «плавающий» характер — например, одна попытка из семи завершается успехом, а остальные завершаются аварийно.
В этой статье мы подробно разберем, почему возникает этот «призрачный глюк», проанализируем сетевые механизмы работы FTP-протокола и выработаем надежные практические решения для платформы 1С:Предприятие.
Для того чтобы найти верное решение, давайте сначала разберем физику процесса. Протокол FTP принципиально отличается от большинства других протоколов (например, HTTP) тем, что использует два типа каналов:
Когда мы инициализируем объект FTPСоединение в режиме пассивного обмена (параметр ПассивныйРежим установлен в значение Истина), события развиваются по следующему сценарию:
Получить() 1С просит сервер открыть порт для передачи данных.В этот момент и кроется главная проблема: если на стороне провайдера, сетевого шлюза (NAT) или брандмауэра FTP-сервера открыта только часть портов динамического пула, то подключение к некоторым случайно выбранным портам будет блокироваться. Когда сервер выделяет «открытый» порт — скачивание проходит успешно. Когда сервер выделяет заблокированный порт — 1С незамедлительно возвращает ошибку доступа к ресурсу.
Дополнительными факторами нестабильности могут выступать:
/otchet), то использование абсолютных путей вида /otchet/sprut.mdb приводит к путанице в адресации на стороне сервера..mdb) могут быть временно заблокированы другими процессами на сервере в момент их обновления, что вызывает кратковременный отказ в доступе.TIME_WAIT, из-за чего сервер временно отвергает новые запросы на передачу данных.---
Если у вас нет прямого доступа к администрированию удаленного FTP-сервера и настройке его брандмауэра, наиболее простым и эффективным решением в коде 1С будет организация цикла повторных попыток скачивания файла с небольшой паузой. Поскольку при каждой новой попытке FTP-сервер предлагает новый случайный порт для передачи данных, одна из попыток обязательно попадет на работающий порт и завершится успехом.
Давайте рассмотрим пример надежной реализации такого механизма:
&НаСервере
Процедура СкачатьФайлСПовторением()
ПапкаДляСохранения = "C:/FTP_Downloads";
ИмяФайлаПриемника = ПапкаДляСохранения + "/sprut.mdb";
// Параметры подключения к FTP
СерверАдрес = "109.195.10.19";
ПортПодключения = 49174;
Пользователь = "логин";
Пароль = "пароль";
ПассивныйРежим = Истина;
МаксимальноеКоличествоПопыток = 10;
ИнтервалМеждуПопытками = 3; // время в секундах
УспешноСкачано = Ложь;
Для НомерПопытки = 1 По МаксимальноеКоличествоПопыток Цикл
Попытка
// Инициализируем соединение внутри попытки для чистоты сессии
Соединение = Новый FTPСоединение(СерверАдрес, ПортПодключения, Пользователь, Пароль, , ПассивныйРежим);
// Избегаем жесткой адресации корня, используем относительный путь
Соединение.Получить("sprut.mdb", ИмяФайлаПриемника);
УспешноСкачано = Истина;
ЗаписьЖурналаРегистрации("FTP.Скачивание", Систематичность.Информация, , ,
"Файл успешно скачан на попытке № " + НомерПопытки);
Прервать; // Выходим из цикла, так как файл успешно получен
Исключение
// Фиксируем ошибку в журнале для последующего анализа администратором
ТекстОшибки = ОписаниеОшибки();
ЗаписьЖурналаРегистрации("FTP.Ошибка", Систематичность.Предупреждение, , ,
"Не удалось скачать файл на попытке № " + НомерПопытки + ". Причина: " + ТекстОшибки);
// Если это не последняя попытка — делаем паузу перед следующим шагом
Если НомерПопытки < МаксимальноеКоличествоПопыток Тогда
ПриостановитьВыполнение(ИнтервалМеждуПопытками);
КонецЕсли;
КонецПопытки;
КонецЦикла;
Если Не УспешноСкачано Тогда
ВызватьИсключение "Не удалось получить файл с FTP-сервера после " + МаксимальноеКоличествоПопыток + " попыток.";
КонецЕсли;
КонецПроцедуры
// Вспомогательный метод для организации задержки (паузы)
Процедура ПриостановитьВыполнение(Секунды)
#Если Сервер Тогда
// Безопасная пауза на стороне сервера без перегрузки процессора
Система = Новый СистемнаяИнформация;
// Альтернативный кроссплатформенный вариант ожидания на сервере:
// Используем фоновое задание или ping-ожидание, если платформа ниже 8.3.18
// Для современных платформ доступен метод ожидания через сокеты или стандартную команду:
ВыполнитьПереход = ТекущаяДата() + Секунды;
Пока ТекущаяДата() < ВыполнитьПереход Цикл
// Пустой цикл ожидания (для коротких интервалов до 5 секунд допустимо)
КонецЦикла;
#Иначе
// На клиенте можно использовать предупреждение с таймаутом
ПоказатьПредупреждение(, "Ожидание повторной попытки...", Секунды);
#КонецЕсли
КонецПроцедуры
---
В процессе анализа проблемы выяснилось, что у пользователя ограничен доступ к родительскому каталогу, и он авторизуется сразу в папку otchet. В этом случае указание абсолютного пути /otchet/sprut.mdb в методе Получить() является грубой ошибкой.
Для FTP-сервера домашняя папка пользователя уже является логическим корнем /. Попытка обратиться к /otchet/sprut.mdb заставляет сервер искать папку otchet внутри самой папки otchet (то есть по пути /otchet/otchet/...), к которой прав гарантированно нет.
Давайте перепишем код с использованием правильного позиционирования и относительных путей:
// Настраиваем соединение без явного указания корневых слешей в методах получения
Соединение = Новый FTPСоединение("109.195.10.19", 49174, "логин", "пароль", , Истина);
// Поскольку авторизация сразу перенаправляет в нужную папку, файл лежит прямо в текущем каталоге
ИмяИсточника = "sprut.mdb";
ИмяПриемника = ПапкаДляСохранения + "/sprut.mdb";
Соединение.Получить(ИмяИсточника, ИмяПриемника);
Важное примечание по поводу слешей: Всегда помните, что на FTP-серверах (которые в большинстве своем работают под управлением ОС Linux/Unix) в качестве разделителя каталогов используется исключительно прямой слеш /. Обратный слеш \ допускается использовать только при формировании локального пути сохранения в операционной системе Windows.
---
Если брандмауэр на стороне клиента (то есть там, где запущена 1С) настроен лояльно и имеет статический «белый» IP-адрес, имеет смысл попробовать переключить соединение в активный режим передачи данных. Для этого в конструкторе достаточно передать параметр Ложь в шестом аргументе:
// Шестой параметр отвечает за пассивный режим. Установим его в Ложь (Активный режим)
Соединение = Новый FTPСоединение("109.195.10.19", 49174, "логин", "пароль", , Ложь);
В активном режиме клиент (1С) сам открывает порт для передачи данных и сообщает его серверу, а сервер инициирует входящее подключение к клиенту. Если проблема была связана с некорректным NAT на стороне FTP-сервера, этот шаг полностью устранит сбои.
---
В высоконагруженных системах, где стабильность обмена критична, разработчики часто отказываются от встроенного в 1С объекта FTPСоединение в пользу проверенных системных утилит, таких как curl или wget. Эти консольные приложения гораздо лучше обрабатывают сетевые задержки, умеют автоматически восстанавливать сессии и не зависят от внутренних особенностей реализации сетевых протоколов платформы 1С.
Давайте посмотрим, как можно реализовать гарантированное скачивание файла через консольную утилиту curl, запустив ее прямо из кода 1С:
&НаСервере
Функция СкачатьФайлЧерезCurl() Экспорт
ПапкаДляСохранения = "C:\FTP_Downloads";
ИмяФайлаПриемника = ПапкаДляСохранения + "\sprut.mdb";
// Формируем параметры запуска curl
// -u задает учетные данные
// -o указывает путь назначения для скачиваемого файла
// --silent отключает прогресс-бар в консоли
// --connect-timeout ограничивает время ожидания ответа
ШаблонКоманды = "curl --connect-timeout 15 --retry 5 --silent -u ""%1:%2"" -o ""%3"" ""ftp://%4:%5/sprut.mdb""";
СтрокаКоманды = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(ШаблонКоманды,
"логин",
"пароль",
ИмяФайлаПриемника,
"109.195.10.19",
"49174"
);
// Запускаем приложение в синхронном режиме (ждем завершения работы)
КодВозврата = Неопределено;
ЗапуститьПриложение(СтрокаКоманды, , Истина, КодВозврата);
Если КодВозврата = 0 Тогда
ЗаписьЖурналаРегистрации("FTP.Curl", Систематичность.Информация, , , "Файл успешно скачан через curl.");
Возврат Истина;
Иначе
ЗаписьЖурналаРегистрации("FTP.Curl", Систематичность.Ошибка, , , "Ошибка выполнения curl. Код возврата: " + КодВозврата);
Возврат Ложь;
КонецЕсли;
КонецФункции
Использование параметра --retry 5 на уровне самой утилиты curl заставляет ее автоматически повторять попытки скачивания в случае сетевых сбоев, избавляя вас от необходимости писать сложные циклы в коде 1С.