Почему C#-компонента выдает ошибку «Время ожидания истекло» в фоновом задании 1С и как это исправить?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При интеграции 1С с внешними системами через внешние компоненты, написанные на C# (.NET Framework), разработчики нередко сталкиваются со странным поведением: код, который отлично работает при запуске из клиентского приложения или формы обработки, начинает выдавать ошибку «Время ожидания истекло» (WebException / Timeout) при запуске в фоновом задании на сервере 1С.

Проанализируем эту ситуацию, выясним глубинную причину блокировки сетевых запросов и рассмотрим эффективные способы решения проблемы как на стороне C#-компоненты, так и средствами платформы 1С:Предприятие.

Симптомы проблемы и первичная диагностика

Рассмотрим характерный сценарий: внешняя компонента выполняет HTTPS-запрос к стороннему REST-сервису внутри цикла для последовательной загрузки массива данных или сообщений. При вызове этой функции из клиентского сеанса 1С всё проходит успешно. Однако при выполнении аналогичного кода на стороне сервера в фоновом задании происходят следующие события:

  1. Первый и второй запросы к серверу обрабатываются успешно.
  2. На третьей итерации цикла выполнение компоненты зависает.
  3. По истечении таймаута генерируется исключение с сообщением о превышении времени ожидания.

Разберем, почему один и тот же код ведет себя по-разному на клиенте и в сервере 1С, и почему сбой происходит именно на третьем запросе.

Глубинная причина: пул соединений в .NET и незакрытые сокеты

Проанализируем архитектуру работы сетевого стека .NET Framework. Класс HttpWebRequest для управления HTTP/HTTPS-соединениями использует глобальный пул подключений ServicePointManager.

По умолчанию в .NET Framework установлено жесткое ограничение на максимальное количество одновременных активных соединений к одному IP-адресу или хосту: ServicePointManager.DefaultConnectionLimit = 2.

Если в коде компоненты после получения ответа от сервера явно не закрываются объекты HttpWebResponse, Stream и StreamReader, сетевой сокет остаётся занятым в пуле .NET, ожидая автоматической сборки мусора (Garbage Collector):

  1. 1-й запрос: занимает слот 1 в пуле соединений.
  2. 2-й запрос: занимает слот 2 в пуле соединений.
  3. 3-й запрос: пытается открыть новое соединение, но оба слота пула заняты незакрытыми предыдущими запросами. Запрос встает в очередь ожидания освобождения слота.
  4. Поскольку предыдущие сокеты удерживаются незакрытыми объектами, время ожидания истекает, и .NET генерирует исключение WebException: Время ожидания истекло.

Почему на клиенте код мог работать? На клиентской машине или в интерфейсном потоке паузы между вызовами и работа сборщика мусора успевают очищать незакрытые ресурсы. На сервере 1С (в рабочем процессе rphost) фоновые задания выполняются с высокой интенсивностью, из-за чего сборщик мусора не успевает освобождать сокеты до исчерпания пула.

Решение 1. Явное освобождение ресурсов в коде C#

Самое главное правило при работе с сетевыми потоками в .NET — гарантированное закрытие и очистка сокетов. Если вы используете явный вызов методов закрытия, код компоненты должен закрывать все созданные объекты ответа.

В исходном варианте компоненты ресурсы остаются открытыми:


// Опасный код: ресурсы не освобождаются
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
StreamReader responseReader = new StreamReader(response.GetResponseStream());
message.Строка = responseReader.ReadToEnd();
// Отсутствует закрытие responseReader, response и потока!

Для исправления ошибки необходимо добавить явное закрытие и освобождение всех связанных ресурсов:


// Корректное закрытие ресурсов
responseReader.Close();
responseReader.Dispose();
stream.Close();
stream.Dispose();
response.Close();

Чтобы гарантировать освобождение сокета даже при возникновении ошибки (исключения) в процессе чтения данных, разберем более надежный подход — применение конструкции using:


using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
using (Stream stream = response.GetResponseStream())
using (StreamReader responseReader = new StreamReader(stream))
{
    message.Строка = responseReader.ReadToEnd();
    message.Код = (int)response.StatusCode;
    message.КодСообщения = response.Headers["Jmsmessageid"];
}

Конструкция using автоматически вызывает метод Dispose() для каждого объекта при выходе из блока, что мгновенно возвращает сокет в пул и предотвращает зависание на третьем запросе.

Решение 2. Настройка лимитов ServicePointManager в C#

Если компонента выполняет интенсивный параллельный или высокочастотный обмен данными, рекомендуется также увеличить базовый лимит соединений в коде инициализации компоненты:


// Увеличиваем лимит параллельных соединений к одному хосту
ServicePointManager.DefaultConnectionLimit = 100;

// Настраиваем время жизни и простоя сокетов
request.ServicePoint.ConnectionLeaseTimeout = 5000; // мс
request.ServicePoint.MaxIdleTime = 5000;           // мс

Обратите внимание: Назначение проверки сертификатов через ServicePointManager.ServerCertificateValidationCallback действует глобально на весь процесс. В многопоточной среде рабочего процесса 1С (rphost) это может затронуть другие фоновые задания. Для новых разработок рекомендуется переходить с устаревшего HttpWebRequest на класс HttpClient с настройкой HttpClientHandler.ServerCertificateCustomValidationCallback.

Решение 3. Альтернатива — использование штатных объектов 1С

Проанализируем целесообразность использования внешних C#-компонент. Если логика взаимодействия с REST API не содержит специфичных алгоритмов, требующих исключительно .NET, гораздо надежнее реализовать запросы средствами самой платформы 1С.

Платформа 1С:Предприятие содержит встроенные объекты HTTPСоединение, HTTPЗапрос и ЗащищенноеСоединениеOpenSSL, которые автоматически и корректно управляют сетевыми сокетами и пулом подключений.

Рассмотрим пример выполнения аналогичного HTTPS-запроса на языке 1С:


// Настройка защищенного SSL-соединения
ЗащищенноеСоединение = Новый ЗащищенноеСоединениеOpenSSL();

// Создание HTTP-соединения
Соединение = Новый HTTPСоединение(
    Сервер, 
    Порт, 
    Логин, 
    Пароль, 
    , 
    , 
    ЗащищенноеСоединение
);

// Формирование и отправка запроса
Заголовки = Новый Соответствие();
Заголовки.Вставить("Content-Type", "application/json");

ПутьКРесурсу = "/v1/peek/" + Ресурс + ".json";
Запрос = Новый HTTPЗапрос(ПутьКРесурсу, Заголовки);

Ответ = Соединение.ВызватьHTTPМетод("GET", Запрос);

КодСостояния = Ответ.КодСостояния;
ТекстОтвета  = Ответ.ПолучитьТелоКакСтроку();

Преимущества использования встроенных объектов 1С:

Таким образом, для исправления зависания C#-компоненты на сервере достаточно обернуть работу со всеми сетевыми потоками в блоки using или явно вызывать методы Close() и Dispose().

← На главную