WebClient超时花费的时间比预期的要长(使用:Rx、Try-Catch、Task)



问题:我在ExtendedWebClient中继承了WebClient,在GetWebRequest方法中重写了WebRequest的超时属性。如果我把它设置为100毫秒,甚至20毫秒,它总是至少需要30秒以上。有时它似乎根本无法通过。

此外,当提供图像的服务(见下面的代码)再次联机时,Rx/System.Reactive中编写的代码不再将图像推入pictureBox?

我该怎么解决这个问题,我做错了什么?(参见下面的代码)


测试用例:我为此设置了一个WinForms测试项目,该项目正在执行以下操作。

  1. GetNextImageAsync

    public async Task<Image> GetNextImageAsync()
    {            
        Image image = default(Image);
        try {
            using (var webClient = new ExtendedWebClient()) {                    
                var data = await webClient.DownloadDataTaskAsync(new Uri("http://SOMEIPADDRESS/x/y/GetJpegImage.cgi"));
                image = ByteArrayToImage(data);
                return image;
            }
        } catch {
            return image;
        }
    }
    
  2. 扩展的WebClient

    private class ExtendedWebClient : WebClient
    {
        protected override WebRequest GetWebRequest(Uri address)
        {                
            var webRequest = base.GetWebRequest(address);
            webRequest.Timeout = 100;
            return webRequest;
        }
    }
    

3.1演示代码(使用Rx)

注意:它实际上从未到达pictures.Subscribe()正文中的"else"语句。

    var pictures = Observable
            .FromAsync<Image>(GetNextImageAsync)
            .Throttle(TimeSpan.FromSeconds(.5))
            .Repeat()
            ;
    pictures.Subscribe(img => {
        if (img != null) {
            pictureBox1.Image = img;
        } else {
            if (pictureBox1.Created && this.Created) {
                using (var g = pictureBox1.CreateGraphics()) {
                    g.DrawString("[-]", new Font("Verdana", 8), Brushes.Red, new PointF(8, 8));
                }
            }
        }
    });

3.2演示代码(使用Task.Run)

注意1:这里调用"else"主体,尽管WebClient的超时时间仍然比预期的要长。。。。

注意2:我不想使用这种方法,因为这样我就不能"节流"图像流,我无法按正确的顺序获取它们,也无法用我的图像流做其他事情。。。但这只是它工作的示例代码。。。

    Task.Run(() => {
        while (true) {
            GetNextImageAsync().ContinueWith(img => {
                if(img.Result != null) {
                    pictureBox1.Image = img.Result;
                } else {
                    if (pictureBox1.Created && this.Created) {
                        using (var g = pictureBox1.CreateGraphics()) {
                            g.DrawString("[-]", new Font("Verdana", 8), Brushes.Red, new PointF(8, 8));
                        }
                    }
                }
            });                    
        }
    });
  1. 作为参考,将byte[]转换为Image对象的代码。

    public Image ByteArrayToImage(byte[] byteArrayIn)
    {
        using(var memoryStream = new MemoryStream(byteArrayIn)){
            Image returnImage = Image.FromStream(memoryStream);
            return returnImage;
        }
    }
    

其他问题

我将在下面解决取消问题,但对以下代码的行为也有误解,无论取消问题如何,这都会导致问题:

var pictures = Observable
  .FromAsync<Image>(GetNextImageAsync)
  .Throttle(TimeSpan.FromSeconds(.5))
  .Repeat()

您可能认为这里的Throttle会限制GetNextImageAsync的调用率。遗憾的是,事实并非如此。考虑以下代码:

var xs = Observable.Return(1)
                   .Throttle(TimeSpan.FromSeconds(5));

您认为在订阅服务器上调用OnNext(1)需要多长时间?如果你认为5秒钟,那你就错了。由于CCD_ 11在其CCD_ 13之后立即发送CCD_。

与此代码相比,我们创建了一个非终止流:

var xs = Observable.Never<int>().StartWith(1)
                   .Throttle(TimeSpan.FromSeconds(5));

现在OnNext(1)在5秒后到达。

所有这一切的结果是,你的无限期Repeat将攻击你的代码,在图像到达时尽快请求图像——这到底是如何造成你所看到的影响,还需要进一步分析。

根据您的要求,有几种结构可以限制查询速率。一种是简单地在结果中添加一个空延迟:

var xs = Observable.FromAsync(GetValueAsync)                       
                   .Concat(Observable.Never<int>()
                        .Timeout(TimeSpan.FromSeconds(5),
                                 Observable.Empty<int>()))
                   .Repeat();

在这里,您可以用GetValueAsync返回的类型替换int

取消

正如@StephenCleary所观察到的,设置WebRequestTimeout属性将仅适用于同步请求。在研究了这些必要的更改以使用WebClient干净地实现取消后,我不得不同意WebClient的问题,如果可能的话,最好转换为HttpClient

可悲的是,即便如此;"容易";诸如CCD_ 24之类的回拉数据的方法(出于某种奇怪的原因)不具有接受CCD_ 25的过载。

如果您确实使用HttpClient,那么超时处理的一个选项是通过Rx,如下所示:

void Main()
{
    Observable.FromAsync(GetNextImageAsync)
            .Timeout(TimeSpan.FromSeconds(1), Observable.Empty<byte[]>())
            .Subscribe(Console.WriteLine);
}
public async Task<byte[]> GetNextImageAsync(CancellationToken ct)
{
    using(var wc = new HttpClient())
    {
        var response = await wc.GetAsync(new Uri("http://www.google.com"),
            HttpCompletionOption.ResponseContentRead, ct);
        return await response.Content.ReadAsByteArrayAsync();
    }
}

在这里,我使用了Timeout运算符来在超时的情况下发出一个空流——根据您的需要,还可以使用其他选项。

Timeout超时时,它将取消对FromAsync的订阅,而CCD_29又将取消它通过GetNextImageAsync间接传递给HttpClient.GetAsync的取消令牌。

您也可以使用类似的构造在WebRequest上调用Abort,但正如我所说,由于没有直接支持取消令牌,这更麻烦。

引用MSDN文档:

Timeout属性仅影响使用GetResponse方法发出的同步请求。要使异步请求超时,请使用Abort方法。

可以乱用Abort方法,但从WebClient转换为HttpClient更容易,因为CCD_36在设计时考虑到了异步操作。

最新更新