对一个 IP 范围的多个 HttpClient 异步请求

Posted

技术标签:

【中文标题】对一个 IP 范围的多个 HttpClient 异步请求【英文标题】:Multiple HttpClient async requests to an ip range 【发布时间】:2015-02-09 22:43:55 【问题描述】:

我正在编写一个应用程序,我必须在其中扫描一个 ip(内部网)范围并查看特定 ip 是否对应于特定 url。例如,假设我们有一个 url:http://<ip>:8080/x/y,我们想看看我们是否可以找到一个在 192.168.1.1 - 192.168.1.254 范围内运行的活动服务器。显然,扫描过程不应阻塞 UI。

所以我创建了以下异步方法:

    private List<AvailableServer> _availableServerList;

    public List<AvailableServer> AvailableServerList
    
        get  return _availableServerList;
        set  _availableServerList = value;
    

    private Mutex objLock = new Mutex(true, "MutexForScanningServers");
    private long IPsChecked;
    private List<string> allPossibleIPs;

    CancellationTokenSource cts;

    public async Task GetAvailableServers()
    
        Dictionary<string, string> localIPs = LocalIPAddress(); //new Dictionary<string, string>();

        allPossibleIPs = new List<string>();

        Dictionary<string, CancellationToken> kvrList = new Dictionary<string, CancellationToken>();
        cts = new CancellationTokenSource();

        foreach (KeyValuePair<string, string> kvp in localIPs)
        
            allPossibleIPs.AddRange(getIpRange(kvp.Key.ToString(), kvp.Value.ToString()));
        

        foreach (string ip in allPossibleIPs)
        
            kvrList.Add(ip, cts.Token);
        

        AvailableServerList = new List<AvailableServer>();

        var downloads = kvrList.Select(kvr => isServerAvailableAsync(kvr));
        Task[] dTasks = downloads.ToArray();
        await Task.WhenAll(dTasks).ConfigureAwait(false);
    

其目的是启动一组任务,每个任务都试图接收有效的 HttpClient 请求。如果 HttpClient.GetStringAsync 没有抛出任何异常,假设请求是有效的:

    private async Task isServerAvailableAsync(Object obj)
    
        using (var client = new HttpClient())
        
            try
            
                KeyValuePair<string, CancellationToken> kvrObj = (KeyValuePair<string, CancellationToken>)obj;
                string urlToCheck = @"http://" + kvrObj.Key + ":8080/x/y";

                string downloadTask = await client.GetStringAsync(urlToCheck).ConfigureAwait(false);

                string serverHostName = Dns.GetHostEntry(kvrObj.Key).HostName;
                AvailableServerList.Add(new AvailableServer(serverHostName, @"http://" + serverHostName + ":8080/x/y"));
               // 
           
            catch (System.Exception ex)
            
                //...Do nothing for now...
            
            finally
            
                lock (objLock)
                
                    //kvrObj.Value.ThrowIfCancellationRequested();
                    IPsChecked++;
                    int tmpPercentage = (int)((IPsChecked * 100) / allPossibleIPs.Count);
                    if (IPsCheckedPercentageCompleted < tmpPercentage)
                    
                        IPsCheckedPercentageCompleted = tmpPercentage;
                        OnScanAvailableServersStatusChanged(new EventArgs());
                    
                
            
        
    

如果请求有效,那么我们找到了可用的服务器,因此我们将 url 添加到我们的列表中。否则我们会捕获异常。最后我们更新百分比变量,因为我们想将它们传递给我们的 UI(xx% 已扫描)。每次任务完成时,它都会触发一个委托,我们的 UI 使用该委托来获取可用服务器的新更新列表和完成百分比。 主要的异步函数 GetAvailableServers 开始通过 Task.Run(() => className.GetAvailableServers()) 运行,它存在于我自己的 ViewModel 中的 DelegateCommand 中:

    public ICommand GetAvailableServersFromViewModel
    
        get
        
            return new DelegateCommand
            
                CanExecuteFunc = () => true,
                CommandAction = () =>
                
                    Task.Run(() => utilsIp.GetAvailableServers());
                
            ;
        
    

我的实现的问题是 UI 在扫描时滞后,这很容易通过我在 UI 中的加载微调器看到。那么代码远不是最好的,我知道我在某个地方错了。

【问题讨论】:

在最大负载下暂停调试器 10 次。每次查看 UI 线程堆栈。什么代码在运行?这就是滞后于 UI 的代码。 我试过了,但我没有找到清楚的东西。我还尝试删除与百分比更新相关的任何 UI 逻辑,并将代码仅保留为 Task.Run(() => utilsIp.GetAvailableServers());我又滞后了。问题似乎是当 await client.GetStringAsync 抛出异常时,(我有大量异常导致扫描过程中 99% 的 IP 根本不存在),但我不确定为什么我有这种行为应该在不同的线程上工作.... 将 URL 更改为始终指向有效的端点。滞后消失了吗?如果是,则错误与问题有关。 我测试了这个场景。一开始我的印象是滞后已经消失了。然后我尝试将具有有效 URL 地址的任务最大化为 5000。然后在 UI 中再次出现同样的问题,这次没有任何例外。我会继续研究... “我没有找到清楚的东西”也许我可以帮忙。如果 UI 滞后,代码必须在 UI 线程上运行。贴几叠,我看看。 【参考方案1】:

如您所知(根据您的代码),当您在 UI 线程上等待任务时,捕获的任务上下文是 UI 上下文,always runs on a single thread。

这意味着当您的任务完成后,控制权会返回到 UI 线程,然后在该线程上运行延续代码。

您可以尝试通过使用.ConfigureAwait(false) 方法(您就是这样)来避免这种情况。该调用关闭了任务线程同步上下文的配置,因此当继续代码准备好运行时,它将使用默认上下文,这意味着它将在线程池而不是任务所在的 UI 线程上执行。

但是,如here 所述(并在here 中暗示),如果任务在等待之前完成,则不会进行线程切换,继续代码将在 UI 线程上执行。

这就是我认为您的问题所在,因为await 代码后面的代码有时可以在 UI 线程上运行。 await 之后的代码是 Dns.GetHostEntry(...) 和 "GetHostEntry is ridiculously slow, particularly when no reverse DNS entry is found"。

从here和here可以看出,“GetHostEntry方法向DNS服务器查询与主机名或IP地址相关联的IP地址”,即网络I/O,即阻塞调用,这意味着 UI 滞后。

这是一个很长的说法,我相信解决这个问题的方法是将 GetHostEntry(...) 包裹在一个任务中,以防第一个 await 没有阻塞并且最终运行UI 线程。

【讨论】:

以上是关于对一个 IP 范围的多个 HttpClient 异步请求的主要内容,如果未能解决你的问题,请参考以下文章

lucene卷曲查询的多个术语

如何在 .net Core API 项目中跨多个线程限制对 HttpClient 的所有传出异步调用

基于 IP 范围的对来自 Azure 上不同存储帐户的其他资源的访问控制

13IP类别和地址

Scapy/ARP 请求不适用于多个/范围 IP。仅针对单个 IP 请求

TCP/IP概述(网络互联与TCP/IP)