.Net WebRequest 超时与 TCP 超时

.Net WebRequest Timeout versus TCP Timeout

问题

如何为单个 WebRequest 配置 TCP 超时?

上下文

根据文档,WebRequest.Timeout:

The length of time, in milliseconds, until the request times out, or the value Timeout.Infinite to indicate that the request does not time out. The default value is defined by the descendant class.

从不存在的端点(IIS 在请求的端口上没有服务绑定)请求 Web 资源失败,超时时间不同且恒定,约为 21 秒

根据这个 serverfault answer 这似乎是连接 TCP 超时。

示例代码

 public static async Task<string> WebRequestToString(string requestUri, int timeoutMilliseconds)
 {
     var request = WebRequest.Create(requestUri) as HttpWebRequest;
     request.KeepAlive = false;
     request.Timeout = timeoutMilliseconds;
     request.ReadWriteTimeout = timeoutMilliseconds;
     using (var response = await request.GetResponseAsync() as HttpWebResponse)
     {
         // Get the response stream
         using (var reader = new StreamReader(response.GetResponseStream()))
         {
             var responseBody = await reader.ReadToEndAsync();
             return responseBody;
         }
     }
 }

 static void Main(string[] args)
 {
     string uri = "http://10.15.1.24:8081/thiservicedoesnotexist/";
     int timeoutMilliseconds = 10;

     Stopwatch sw = new Stopwatch();
     sw.Start();
     try
     {
         WebRequestToString(uri, timeoutMilliseconds).Wait();
     }
     catch (AggregateException ex)
     {
         Console.WriteLine(ex.ToString());
     }
     sw.Stop();

     Console.WriteLine("Elaped {0}ms", sw.ElapsedMilliseconds);
     Console.ReadKey();
 }

示例输出

  System.AggregateException: One or more errors occurred. ---> System.Net.WebExcep
  tion: Unable to connect to the remote server ---> System.Net.Sockets.SocketExcep
  tion: A connection attempt failed because the connected party did not properly r
  espond after a period of time, or established connection failed because connecte
  d host has failed to respond 10.15.1.24:8081
     at System.Net.Sockets.Socket.EndConnect(IAsyncResult asyncResult)
     at System.Net.ServicePoint.ConnectSocketInternal(Boolean connectFailure, Sock
  et s4, Socket s6, Socket& socket, IPAddress& address, ConnectSocketState state,
  IAsyncResult asyncResult, Exception& exception)
     --- End of inner exception stack trace ---
     at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
     at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar,
  Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchron
  ization)
  --- End of stack trace from previous location where exception was thrown ---
     at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
     at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNot
  ification(Task task)
     at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
     at TimeoutTests.Program.<WebRequestToString>d__0.MoveNext() in \vmware-host\
  shared folders\Documents\Visual Studio 2012\Projects\TimeoutTests\TimeoutTests\P
  rogram.cs:line 20
     --- End of inner exception stack trace ---
     at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceled
  Exceptions)
     at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationTo
  ken cancellationToken)
     at System.Threading.Tasks.Task.Wait()
     at TimeoutTests.Program.Main(String[] args) in \vmware-host\shared folders\D
  ocuments\Visual Studio 2012\Projects\TimeoutTests\TimeoutTests\Program.cs:line 4
  0
  ---> (Inner Exception #0) System.Net.WebException: Unable to connect to the remo
  te server ---> System.Net.Sockets.SocketException: A connection attempt failed b
  ecause the connected party did not properly respond after a period of time, or e
  stablished connection failed because connected host has failed to respond 10.15.
  1.24:8081
     at System.Net.Sockets.Socket.EndConnect(IAsyncResult asyncResult)
     at System.Net.ServicePoint.ConnectSocketInternal(Boolean connectFailure, Sock
  et s4, Socket s6, Socket& socket, IPAddress& address, ConnectSocketState state,
  IAsyncResult asyncResult, Exception& exception)
     --- End of inner exception stack trace ---
     at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
     at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar,
  Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchron
  ization)
  --- End of stack trace from previous location where exception was thrown ---
     at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
     at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNot
  ification(Task task)
     at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
     at TimeoutTests.Program.<WebRequestToString>d__0.MoveNext() in \vmware-host\
  shared folders\Documents\Visual Studio 2012\Projects\TimeoutTests\TimeoutTests\P
  rogram.cs:line 20<---

  Elaped 21169ms

我很怀疑这个超时实际上和TCP连接超时是一样的。 webrequest超时的文档如下:

The length of time, in milliseconds, until the request times out, or the value Timeout.Infinite to indicate that the request does not time out. The default value is defined by the descendant class.

这是您请求级别的超时,即 HTTP。可能是与服务器的连接已建立,但创建响应需要很长时间。还有那个时间可以超时。

webrequest 如何处理这个并将其映射到 TCP 取决于实现。 建立连接的 TCP 超时不能是无限的。

我还阅读了以下内容: The Timeout property indicates the length of time, in milliseconds, until the request times out and throws a WebException. The Timeout property affects only synchronous requests made with the GetResponse method. To time out asynchronous requests, use the Abort method.

你是异步的!

我最终通过创建一个基于 approach 的 GetResponseAsync 方法扩展来解决这个问题并且似乎工作正常:

public static class WebRequestExtensions
{
    public static async Task<WebResponse> GetResponseAsyncWithTimeout(this WebRequest request)
    {
        var timeoutCancellationTokenSource = new CancellationTokenSource();

        var responseTask = request.GetResponseAsync();

        var completedTask = await Task.WhenAny(responseTask, Task.Delay(request.Timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == responseTask)
        {
            timeoutCancellationTokenSource.Cancel();
            return await responseTask;
        }
        else
        {
            request.Abort();
            throw new TimeoutException("The operation has timed out.");
        }
    }
}