CURL-запрос с помощью HttpClient — как установить тайм-аут на подключение к серверу (WinInet)

Я отправляю запрос cURL с помощью HttpClient с помощью метода, описанного здесь ниже.

Параметр, используемый для этого метода:

SelectedProxy = пользовательский класс, в котором хранятся параметры моего прокси

Parameters.WcTimeout = время ожидания

url, header, content = запрос cURL (на основе этого инструмента для преобразования в C# https://curl.olsh.me/).

        const SslProtocols _Tls12 = (SslProtocols)0x00000C00;
        const SecurityProtocolType Tls12 = (SecurityProtocolType)_Tls12;
        ServicePointManager.SecurityProtocol = Tls12;
        string source = "";

        using (var handler = new HttpClientHandler())
        {
            handler.UseCookies = usecookies;
            WebProxy wp = new WebProxy(SelectedProxy.Address);
            handler.Proxy = wp;

            using (var httpClient = new HttpClient(handler))
            {
                httpClient.Timeout = Parameters.WcTimeout;

                using (var request = new HttpRequestMessage(new HttpMethod(HttpMethod), url))
                {
                    if (headers != null)
                    {
                        foreach (var h in headers)
                        {
                            request.Headers.TryAddWithoutValidation(h.Item1, h.Item2);
                        }
                    }
                    if (content != "")
                    {
                        request.Content = new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded");
                    }

                    HttpResponseMessage response = new HttpResponseMessage();
                    try
                    {
                        response = await httpClient.SendAsync(request);
                    }
                    catch (Exception e)
                    {
                        //Here the exception happens
                    }
                    source = await response.Content.ReadAsStringAsync();
                }
            }
        }
        return source;

Если я запускаю это без прокси, это работает как шарм. Когда я отправляю запрос с использованием прокси-сервера, который я тестировал первым из Chrome, у меня возникает следующая ошибка при попытке {} catch {}. Вот дерево ошибок

{"An error occurred while sending the request."}
    InnerException {"Unable to connect to the remote server"}
        InnerException {"A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond [ProxyAdress]"}
        SocketErrorCode: TimedOut

Используя секундомер, я вижу, что TimedOut произошел примерно через 30 секунд.


Я попробовал несколько разных обработчиков на основе следующих ссылок В чем разница между HttpClient.Timeout и использованием свойств времени ожидания WebRequestHandler?, Путаница с тайм-аутом HttpClient или с помощью WinHttpHandler.

Стоит отметить, что WinHttpHandler допускает другой код ошибки, т. Е. Ошибка 12002, вызывающая WINHTTP_CALLBACK_STATUS_REQUEST_ERROR, «время ожидания операции истекло». Основная причина та же, хотя она помогла определить, где она ошибается (например, WinInet), что также подтверждает то, что @DavidWright говорил о том, что тайм-ауты от HttpClient управляют другой частью отправки запроса.

Следовательно, моя проблема возникает из-за времени, необходимого для установления соединения с сервером, что вызывает 30-секундный тайм-аут от WinInet.

Тогда мой вопрос: как изменить этот тайм-аут?

Кстати, стоит отметить, что Chrome, использующий WinInet, похоже, не страдает от этого тайм-аута, как и Cefsharp, на котором основана большая часть моего приложения и через который одни и те же прокси-серверы могут правильно отправлять запросы.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
1 086
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

У меня была такая же проблема с HttpClient. Для возврата SendAsync должны произойти две вещи: во-первых, настроить TCP-канал, по которому происходит связь (рукопожатие SYN, SYN/ACK, ACK, если вы знакомы с этим), и, во-вторых, вернуть данные, составляющие HTTP-ответ по этому TCP-каналу. Тайм-аут HttpClient применяется только ко второй части. Тайм-аут для первой части определяется сетевой подсистемой ОС, и изменить этот тайм-аут в коде .NET довольно сложно.

(Вот как вы можете воспроизвести этот эффект. Установите работающее клиент-серверное соединение между двумя машинами, чтобы вы знали, что разрешение имен, доступ к портам, прослушивание и логика клиента и сервера работают. Затем отключите сетевой кабель от сервера и повторно запустите клиентский запрос. Он истечет с тайм-аутом сети по умолчанию ОС, независимо от того, какой тайм-аут вы установили на своем HttpClient.)

Единственный известный мне способ обойти это — запустить собственный таймер задержки в другом потоке и отменить задачу SendAsync, если таймер завершится первым. Вы можете сделать это, используя Task.Delay и Task.WaitAny или создав CancellationTokenSource с желаемым временем (что, по сути, просто делает первый способ под капотом). В любом случае вам нужно быть осторожным с отменой и чтением исключений из задачи, которая проигрывает гонку.

Спасибо @DavidWright За таймер у меня нет проблем установить его, то есть что-то вроде try { Task[] t = new Task[1]; t[0] = Task.Run(async () => [whatever]); Task.WaitAll(t, timeout); } catch { //cancel whatever is still pending }. Но что касается части TCP, я понятия не имею, как с этим бороться;)

samuel guedon 29.05.2019 07:58

@samuelguedon: ваш таймер (если он установлен на достаточно небольшую продолжительность) вернется до истечения времени ожидания TCP. Затем вы можете отменить задачу SendAsync и двигаться дальше. Вы обойдёте тот факт, что SendAsync не вернётся до истечения тайм-аута TCP.

David Wright 29.05.2019 20:52

мне кажется, что существует, скажем, 30-секундный тайм-аут TCP, и что вы говорите мне реализовать более короткий тайм-аут и вручную убить SendAsync. Но тогда у меня не будет результата. Моя цель состоит в том, чтобы иметь тайм-аут 60, 90 секунд (дольше, чем текущий TCP), чтобы SendAsync вернулся ко мне. Может быть, я что-то упустил в вашем объяснении;)

samuel guedon 29.05.2019 23:07

после проверки я немного лучше понимаю то, что вы упомянули. Соединение с сервером управляется движком WinInet Replay, который имеет 30-секундный тайм-аут для соединения с сервером. Хотя я все еще пытаюсь найти, как изменить его на 90 секунд. Я нашел несколько обработчиков, которые позволяют более детально подойти к таймауту, но это все еще не на должном уровне.

samuel guedon 30.05.2019 22:37

@samuelguedon: Извините, я думал, вы хотите сократить время ожидания. Увеличить время ожидания TCP будет сложно, потому что, когда код .NET (ваш или HttpClient) получит ошибку от подсистемы TCP по истечении времени ожидания. Таким образом, вы можете либо (1) повторить попытку до желаемого тайм-аута, либо (2) использовать настройки реестра и перезагрузиться, чтобы получить новый тайм-аут TCP.

David Wright 31.05.2019 00:16

@samuelguedon: см. serverfault.com/questions/193160/… для получения информации о перенастройке ОС.

David Wright 31.05.2019 00:18

относительно двух вариантов: (1) я не могу повторно отправить запрос, так как я получил сообщение об ошибке «Сообщение с запросом уже отправлено. Невозможно отправить одно и то же сообщение с запросом несколько раз», если я возвращаюсь назад и (2) не могу найти ключи реестра, упомянутые в предоставленной ссылке. Нет ни TcpInitialRTT, ни TcpMaxConnectRetransmissions ни в HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services:\Tcpip\Parameters, ни где-либо еще

samuel guedon 31.05.2019 00:51

@samuelguedon: чтобы повторить попытку, вы не можете повторно использовать один и тот же экземпляр HttpRequestMessage (HttpClient эффективно помечает его как использованный и выдает полученное исключение, если вы снова передаете его в SendAsync), но ничто не мешает вам создать новый, эквивалентный HttpRequestMessage и отправив его. (Если это не GET, вам нужно учитывать идемпотентность.)

David Wright 31.05.2019 01:02

@samuelguedon: Что касается варианта (2), у меня больше нет опыта. Я не пошел по этому пути, потому что мне нужно было, чтобы мой код работал на машинах, на которых я не мог настроить такие параметры.

David Wright 31.05.2019 01:03

Хорошо, я вернусь назад до создания HttpRequestMessage. Поэтому я должен ожидать, что что-то вроде 1-го запроса инициирует соединение с сервером, истечет время ожидания через 30 секунд; 2-й запрос каким-то образом выиграет от 1-й попытки и, следовательно, 2-й (или 3-й, или любой другой в течение моего собственного тайм-аута) наконец пройдет? Как бы странно это ни звучало (для меня), я не удивлен, учитывая, что после нескольких попыток моего предыдущего кода ему каким-то образом удалось пройти до истечения времени ожидания. Завтра проверю (сейчас мой прокси проходит с первого раза :) )

samuel guedon 31.05.2019 01:06
Ответ принят как подходящий

Итак, благодаря @DavidWright я понял несколько вещей:

  1. Перед этим отправляется HttpRequestMessage и начинается тайм-аут от HttpClient, инициируется TCP-соединение с сервером
  2. TCP-соединение имеет свой собственный тайм-аут, определенный на уровне ОС, и мы не определили способ изменить его во время выполнения с C# (ожидающий ответа вопрос, если кто-то хочет внести свой вклад)
  3. Настаивание на попытке подключения работает, поскольку каждая попытка выигрывает от предыдущих попыток, хотя необходимо реализовать надлежащее управление исключениями и ручной счетчик времени ожидания (я фактически учел количество попыток в своем коде, предполагая, что каждая попытка составляет около 30 секунд)

Все это вместе вылилось в следующий код:

        const SslProtocols _Tls12 = (SslProtocols)0x00000C00;
        const SecurityProtocolType Tls12 = (SecurityProtocolType)_Tls12;
        ServicePointManager.SecurityProtocol = Tls12;
        var sp = ServicePointManager.FindServicePoint(endpoint);

        sp.ConnectionLeaseTimeout = (int)Parameters.ConnectionLeaseTimeout.TotalMilliseconds;


        string source = "";

        using (var handler = new HttpClientHandler())
        {
            handler.UseCookies = usecookies;
            WebProxy wp = new WebProxy(SelectedProxy.Address);
            handler.Proxy = wp;

            using (var client = new HttpClient(handler))
            {
                client.Timeout = Parameters.WcTimeout;

                int n = 0;
                back:
                using (var request = new HttpRequestMessage(new HttpMethod(HttpMethod), endpoint))
                {

                    if (headers != null)
                    {
                        foreach (var h in headers)
                        {
                            request.Headers.TryAddWithoutValidation(h.Item1, h.Item2);
                        }
                    }
                    if (content != "")
                    {
                        request.Content = new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded");
                    }
                    HttpResponseMessage response = new HttpResponseMessage();

                    try
                    {
                        response = await client.SendAsync(request);
                    }
                    catch (Exception e)
                    {
                        if (e.InnerException != null)
                        {
                            if (e.InnerException.InnerException != null)
                            {
                                if (e.InnerException.InnerException.Message.Contains("A connection attempt failed because the connected party did not properly respond after"))
                                {
                                    if (n <= Parameters.TCPMaxTries)
                                    {
                                        n++;
                                        goto back;
                                    }
                                }
                            }
                        }
                        // Manage here other exceptions
                    }
                    source = await response.Content.ReadAsStringAsync();
                }
            }
        }
        return source;

Кстати, моя текущая реализация HttpClient может вызвать проблемы в будущем. Несмотря на то, что HttpClient является одноразовым, его следует определять на уровне приложения с помощью статики, а не в операторе using. Чтобы узнать больше об этом, перейдите по ссылке здесь или там.

Моя проблема в том, что я хочу обновлять прокси-сервер при каждом запросе и что он не устанавливается для каждого запроса. Хотя это объясняет причину нового параметра ConnectionLeaseTimeout (чтобы свести к минимуму время, в течение которого аренда остается открытой), это другая тема.

Другие вопросы по теме