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



我正在编写一个应用程序,在该应用程序中,我必须扫描ip(intranet)范围,看看特定的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())开始运行,该函数存在于DelegateCommand中,该命令驻留在我自己的ViewModel中:

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

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

正如您所知(基于您的代码),当您在UI线程上等待任务时,捕获的任务上下文就是UI上下文,它总是在单个线程上运行。

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

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

然而,正如这里所描述的(以及这里所暗示的),如果任务在等待之前完成,那么就不会进行线程切换,并且继续代码将在UI线程上执行。

这就是我认为您的问题所在,因为await代码后面的代码有时可以在UI线程上运行。await后面的代码是Dns.GetHostEntry(...),"GetHostEntry慢得离谱,尤其是在找不到反向DNS条目的情况下"。

我们可以从这里和这里看到,"GetHostEntry方法向DNS服务器查询与主机名或IP地址相关的IP地址",这意味着网络I/O,意味着阻止调用,意味着滞后的UI。

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

最新更新