【问题标题】:SocketException existing connection forcibly closed with FlurlSocketException 现有连接被 Flurl 强行关闭
【发布时间】:2020-01-02 18:03:39
【问题描述】:

我正在尝试使用 Flurl 作为我的 HTTP 客户端库版本 2.4.2 来查询公共 NPPES NPI 注册表。

在 4-5 次成功的异步请求(通过单步执行循环中的每个请求进行调试)后,它总是会失败,并出现 SocketException 表示连接被强制关闭。我假设 API 是速率受限的,但减慢请求似乎不起作用。这是我的相关代码。

static async Task<Result> GetNpiEntry(string npiNumber)
{
    var npiEntry = await "https://npiregistry.cms.hhs.gov"
        .AppendPathSegment("api/")
        .SetQueryParams(new { version = "2.1", number = npiNumber }).GetJsonAsync<RootObject>();

    return npiEntry.results[0];
}

循环调用它并在请求之间进行大量睡眠。

List<Result> npiResults = new List<Result>(npiTable.Rows.Count);

foreach (DataRow row in npiTable.Rows)
{
    Result npiEntry = Task.Run(() => GetNpiEntry((string)row[0])).Result;
    npiResults.Add(npiEntry);
    Thread.Sleep(2000);
}

这是实际的例外。

 ---> System.Net.Http.HttpRequestException: An error occurred while sending the request.
 ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host..
 ---> System.Net.Sockets.SocketException (10054): An existing connection was forcibly closed by the remote host.  

我是否有更合适的方式来调试或执行此操作? 我想我需要对客户端进行速率限制,但是请求之间的宽大等待时间至少对于调试来说不应该起作用吗?

【问题讨论】:

  • 欢迎来到 Stack Overflow!感谢您在问题中发布代码和详细的错误信息,这样更容易回答。 :)
  • 你能不能让你的Task.Run lambda async 然后await 调用GetNpiEntry。即:Task.Run( async ( ) =&gt; await GetNpiEntry
  • @WBuck 我无法添加,因为它无法编译,foreach 循环位于static void Main(string[] args) {} 入口点内。在 Task.Run() 调用之前添加另一个睡眠允许它在遇到此错误之前完成约 1000 个请求,但它仍然会遇到此错误。
  • 我正在发布答案,请稍等
  • Wbuck 的回答是正确的。我在我的中添加了一些细节和复制。希望这对@tNAPA 有帮助!

标签: c# .net-core async-await flurl


【解决方案1】:

您没有在 Task.Run lambda 中 await 调用 GetNpiEntry。如果对GetNpiEntry 的调用同步执行,那么您就没有问题。如果对GetNpiEntry 的调用异步执行,那么Task.Run lambda 将不会是await 的结果。

请尝试以下方法:

foreach (DataRow row in npiTable.Rows)
{
    Result npiEntry = Task.Run( async () => await GetNpiEntry((string)row[0])).GetAwaiter( ).GetResult( );
    npiResults.Add(npiEntry);
    Thread.Sleep(2000);
}

我还看到您正在使用.NetCore,因此您应该可以改用以下main

static async Task Main( string[ ] args )

这允许您在Main 中使用await

【讨论】:

  • 感谢处理结果的工作,但是在(在本例中为 101)请求后,远程主机关闭了套接字并引发了异常。发生这种情况时我应该如何处理重新打开一个新套接字,更直接地说,为什么这会失败,而我可以通过一个较少程序化的客户端在大约 2 分钟内检索大约 2000 个 NPI(顺序)而没有问题,但是在请求之间等待几秒钟是绑定的在这个 C# 版本中几分钟后失败。
  • 发生网络异常。它只是那些经常失败的事情之一。 catch Exception 由您决定。一旦被抓住,您需要决定如何从中恢复。通常我会有“重试”逻辑,它会在失败时尝试相同的调用N 次。通常我会先尝试重新连接remote,连接成功后我会重新发出请求。
  • 一个简单的 try {} catch { wait and retry } 似乎就足够了。谢谢。
【解决方案2】:

问题不在于 Flurl 或 API 限制,而是阻塞异步调用(正如 @WBuck 所说)。

最佳做法是避免使用.Result,甚至避免使用.GetAwaiter().GetResult()。那些和Task.Run() 都是混合同步和异步代码的例子。网上有很多很好的文章来说明为什么这是不好的 - 搜索 .net mix sync async 以获取更多背景信息。

正确的解决方案是“一直向下”使用异步。现在几乎每个入口点都可以标记为async,包括控制台应用程序。

即使没有睡眠,这也适用于我的机器:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Flurl;
using Flurl.Http;

namespace SO_59567958
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var ids = new[] { "1023086709", "1659325215", "1912946427", "1740219450", "1750497640", "1538260823", "1275625626", "1144303488", "1205919107", "1730281890", "1568453561" };
            Console.WriteLine($"Retrieving {ids.Length} NPI records...");

            var npiResults = new List<Result>();
            foreach (var id in ids)
            {
                var retrievedFromApi = await GetNpiEntry(id);
                npiResults.Add(retrievedFromApi);
            }

            Console.WriteLine("Done");
            npiResults.ForEach(x => Console.WriteLine(x.Number));
        }

        static async Task<Result> GetNpiEntry(string npiNumber)
        {
            var npiEntry = await "https://npiregistry.cms.hhs.gov"
                .AppendPathSegment("api/")
                .SetQueryParams(new { version = "2.1", number = npiNumber })
                .GetJsonAsync<RootObject>();

            return npiEntry.Results[0];
        }
    }

    public class RootObject
    {
        public int ResultCount { get; set; }

        public IReadOnlyList<Result> Results { get; set; }
    }

    public class Result
    {
        public int Number { get; set; }
    }
}

【讨论】:

  • 感谢您解释如何正确执行等待异步调用。然而,这个例子在经过足够多的迭代后仍然失败,并出现相同的套接字错误。我有一个不太有用的实现(由另一个程序生成的 VB.NET),它可以在大约 2 分钟内完成查询整个数据集(约 2000 个条目)而没有错误或任何睡眠,但使用此连接被强制关闭经过几十次查询。
  • 我认为我能够很好地处理它,只需捕获异常并等待再试一次,谢谢。
  • 不客气!如需更复杂的重试策略,请查看:thepollyproject.org
猜你喜欢
  • 2013-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-14
  • 2021-05-12
相关资源
最近更新 更多