简体   繁体   English

cURL 使用 HttpClient 请求 - 如何设置服务器连接超时 (WinInet)

[英]cURL request with HttpClient - how to set the timeout on the server connection (WinInet)

I am sending cURL request using HttpClient through the method described here under.我通过这里描述的方法使用 HttpClient 发送 cURL 请求。

The parameter used for this method are:此方法使用的参数是:

SelectedProxy = a custom class that stores my proxy's parameters SelectedProxy = 存储我的代理参数的自定义 class

Parameters.WcTimeout = the timeout Parameters.WcTimeout = 超时时间

url, header, content = the cURL request (based on this tool to convert to C# https://curl.olsh.me/ ). url, header, content = the cURL request (based on this tool to convert to 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;

If I am running this without proxy, it works like a charm.如果我在没有代理的情况下运行它,它就像一个魅力。 When I send a request using a proxy which I tested first from Chrome, I have the following error on my try {} catch {}.当我使用我首先从 Chrome 测试的代理发送请求时,我的 try {} catch {} 出现以下错误。 Here is the error tree这是错误树

{"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

By using a Stopwatch I see that the TimedOut occurred after around 30 sec.通过使用秒表,我看到 TimedOut 发生在大约 30 秒后。


I tried a few different handler based on the following links What's the difference between HttpClient.Timeout and using the WebRequestHandler timeout properties?我根据以下链接尝试了一些不同的处理程序HttpClient.Timeout 和使用 WebRequestHandler 超时属性有什么区别? , HttpClient Timeout confusion or with the WinHttpHandler. , HttpClient Timeout 混淆或与 WinHttpHandler 混淆。

It's worth noting that WinHttpHandler allow for a different error code, ie Error 12002 calling WINHTTP_CALLBACK_STATUS_REQUEST_ERROR, 'The operation timed out'.值得注意的是,WinHttpHandler 允许使用不同的错误代码,即 Error 12002 calls WINHTTP_CALLBACK_STATUS_REQUEST_ERROR, 'The operation timed out'。 The underlying reason is the same though it helped to target where it bugs (ie WinInet) which confirms also what @DavidWright was saying regarding that timeouts from HttpClient manages a different part of the request sending.根本原因是相同的,尽管它有助于定位它的错误位置(即 WinInet),这也证实了@DavidWright 所说的关于来自 HttpClient 的超时管理请求发送的不同部分的内容。

Hence my issue is coming from the time it takes to establish a connection to the server, which triggers the 30sec timeout from WinInet.因此,我的问题来自与服务器建立连接所需的时间,这会触发 WinInet 的 30 秒超时。

My question is then How to change those timeout?我的问题是如何更改那些超时?

On a side note, it's worth noting that Chrome, which uses WinInet, does not seem to suffer from this timeout, nor Cefsharp on which a big part of my app is based, and through which the same proxies can properly send requests.在旁注中,值得注意的是,使用 WinInet 的 Chrome 似乎并没有受到这种超时的影响,我的应用程序的很大一部分所基于的 Cefsharp 也没有受到影响,并且相同的代理可以通过它正确发送请求。

I have had the same problem with HttpClient.我对 HttpClient 也有同样的问题。 Two things need to happen for SendAsync to return: first, setting up the TCP channel over which the communication occurs (the SYN, SYN/ACK, ACK handshake, if you're familiar with that) and second getting back the data that constitutes the HTTP response over that TCP channel.要返回 SendAsync 需要做两件事:首先,设置发生通信的 TCP 通道(SYN、SYN/ACK、ACK 握手,如果您熟悉的话),其次取回构成HTTP 响应 TCP 通道。 HttpClient's timeout only applies to the second part. HttpClient 的超时仅适用于第二部分。 The timeout for the first part is governed by the OS's network subsystem, and it's quite difficult to change that timeout in .NET code.第一部分的超时由操作系统的网络子系统控制,在 .NET 代码中更改该超时非常困难。

(Here's how you can reproduce this effect. Set up a working client/server connection between two machines, so you know that name resolution, port access, listening, and client and server logic all works. Then unplug the network cable on the server and re-run the client request. It will time out with the OS's default network timeout, regardless of what timeout you set on your HttpClient.) (这里是重现这种效果的方法。在两台机器之间建立一个有效的客户端/服务器连接,这样你就知道名称解析、端口访问、侦听以及客户端和服务器逻辑都可以工作。然后拔下服务器上的网线,然后重新运行客户端请求。无论您在 HttpClient 上设置什么超时,它都会以操作系统的默认网络超时超时。)

The only way I know around this is to start your own delay timer on a different thread and cancel the SendAsync task if the timer finishes first.我知道的唯一方法是在不同的线程上启动您自己的延迟计时器,如果计时器首先完成,则取消 SendAsync 任务。 You can do this using Task.Delay and Task.WaitAny or by creating a CancellationTokenSource with your desired timeone (which essentially just does the first way under the hood).您可以使用 Task.Delay 和 Task.WaitAny 或通过使用您想要的时间创建一个 CancellationTokenSource 来执行此操作(这实际上只是在引擎盖下执行第一种方式)。 In either case you will need to be careful about cancelling and reading exceptions from the task that loses the race.在任何一种情况下,您都需要小心取消和读取输掉比赛的任务的异常。

So thanks to @DavidWright I understand a few things:所以感谢@DavidWright,我明白了一些事情:

  1. Before that the HttpRequestMessage is sent and the timeout from HttpClient starts, a TCP connection to the server is initiated在发送HttpRequestMessage并从HttpClient开始超时之前,将启动与服务器的 TCP 连接
  2. The TCP connection has its own timeout, defined at OS level, and we do not identified a way to change it at run time from C# (question pending if anyone want to contribute) TCP 连接有自己的超时,在操作系统级别定义,我们没有确定在运行时从 C# 更改它的方法(如果有人想贡献,问题待定)
  3. Insisting on trying to connect works as each try benefits from previous tries, though proper exception management & manual timeout counter needs to be implemented (I actually considered a number of tries in my code, assuming each try is around 30sec)坚持尝试连接工作,因为每次尝试都受益于以前的尝试,尽管需要实施适当的异常管理和手动超时计数器(我实际上在我的代码中考虑了多次尝试,假设每次尝试大约 30 秒)

All this together ended up in the following code:所有这一切都以以下代码结束:

        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;

On a side note, my current implementation of HttpClient may be problematic in the future.附带说明一下,我当前的HttpClient实现将来可能会出现问题。 Though being disposable, HttpClient should be defined at App level through a static, and not within a using statement.尽管是一次性的, HttpClient应该通过 static 在 App 级别定义,而不是在using语句中。 To read more about this go here orthere .在此处此处阅读有关此 go 的更多信息。

My issue is that I want to renew the proxy at each request and that it is not set on a per request basis.我的问题是我想在每个请求时更新代理,并且它不是基于每个请求设置的。 While it explains the reasdon of the new ConnectionLeaseTimeout parameter (to minimize the time the lease remains open) it is a different topic虽然它解释了新的 ConnectionLeaseTimeout 参数的原因(以最小化租约保持打开的时间),但它是一个不同的主题

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM