You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何通过ReadAsStreamAsync限制HttpClient的下载速度

Great question—thread sleep is indeed a blunt tool for throttling because it doesn’t account for actual network speed fluctuations. Let’s break down better, more precise approaches using HttpClient and stream wrapping that avoid over-throttling while keeping your download speed capped.

1. Wrap the Network Stream in a Throttling Stream (Most Precise Approach)

The cleanest way to control download speed without messing with HttpClient internals is to create a custom Stream subclass that enforces a maximum bytes-per-second limit. This works by tracking the time and bytes read, then dynamically pausing (using async delays, not blocking sleep) to stay under the limit.

Step 1: Implement the ThrottledStream

Here’s a reusable async-friendly throttling stream that calculates delays based on real-time read rates:

public class ThrottledStream : Stream
{
    private readonly Stream _innerStream;
    private readonly long _maxBytesPerSecond;
    private readonly Stopwatch _stopwatch = new Stopwatch();
    private long _totalBytesRead = 0;

    public ThrottledStream(Stream innerStream, long maxBytesPerSecond)
    {
        _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream));
        _maxBytesPerSecond = maxBytesPerSecond > 0 ? maxBytesPerSecond : throw new ArgumentOutOfRangeException(nameof(maxBytesPerSecond));
        _stopwatch.Start();
    }

    public override bool CanRead => _innerStream.CanRead;
    public override bool CanSeek => _innerStream.CanSeek;
    public override bool CanWrite => _innerStream.CanWrite;
    public override long Length => _innerStream.Length;
    public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }

    public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        int bytesRead = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
        if (bytesRead == 0) return 0;

        _totalBytesRead += bytesRead;
        long elapsedMs = _stopwatch.ElapsedMilliseconds;
        double expectedMs = (_totalBytesRead / (double)_maxBytesPerSecond) * 1000;

        // If we're ahead of schedule, wait the difference to stay under the speed limit
        if (expectedMs > elapsedMs)
        {
            long delayMs = (long)(expectedMs - elapsedMs);
            await Task.Delay(delayMs, cancellationToken);
        }

        return bytesRead;
    }

    // Delegate remaining stream methods to the inner stream
    public override void Flush() => _innerStream.Flush();
    public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
    public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
    public override void SetLength(long value) => _innerStream.SetLength(value);
    public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
    protected override void Dispose(bool disposing)
    {
        if (disposing) _innerStream.Dispose();
        base.Dispose(disposing);
    }
}

Step 2: Integrate It Into Your Download Code

Modify your existing code to wrap the content stream with ThrottledStream—this requires minimal changes to your current logic, plus fixes the blocking .Result call that could cause deadlocks:

var request = new HttpRequestMessage(HttpMethod.Get, $"/api/device/DownloadFile/?fileID={fileID}");
using (HttpResponseMessage response = await client.GetAsync(request.RequestUri, HttpCompletionOption.ResponseHeadersRead))
{
    try
    {
        if (response.IsSuccessStatusCode)
        {
            Logging.LogDebug($"[DownloadFile] Starting download of '{fileID}'...");
            int downBuffer = 8192; // Larger buffer improves efficiency for large files
            long maxBytesPerSecond = 10 * 1024 * 1024; // Example: 10 MB/s limit (adjust as needed)

            using (Stream contentStream = await response.Content.ReadAsStreamAsync(),
                   throttledStream = new ThrottledStream(contentStream, maxBytesPerSecond),
                   fileStream = new FileStream(fileLocationTemp.FullName, FileMode.Create, FileAccess.Write, FileShare.None, downBuffer, true))
            {
                var totalRead = 0L;
                var totalReads = 0L;
                var buffer = new byte[downBuffer];
                int read;

                while ((read = await throttledStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                {
                    await fileStream.WriteAsync(buffer, 0, read);
                    totalRead += read;
                    totalReads += 1;

                    if (totalReads % 2000 == 0)
                    {
                        Logging.LogDebug($"[DownloadFile] Total bytes downloaded so far: {totalRead:n0}");
                    }
                }
            }
        }
        else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            Logging.LogDebug($"[DownloadFile] Failed to download file '{fileID}', not found on server.");
            deleteFilesOnFinal = true;
            return false;
        }
        else
        {
            Logging.LogDebug($"[DownloadFile] Failed to download file '{fileID}', status code '{response.StatusCode.ToString()}'.");
            deleteFilesOnFinal = true;
            return false;
        }
    }
    catch (Exception ex)
    {
        Logging.LogDebug($"[DownloadFile] Failed to download file '{fileID}'");
        Logging.LogError(ex);
        return false;
    }
}

2. Alternative: Coarse-Grained Connection Limiting (Less Precise)

If you want a simpler approach for multi-file downloads, you can tweak HttpClient's connection limits to restrict concurrent transfers to the API host:

// Limit concurrent connections to your API server
ServicePointManager.FindServicePoint(new Uri("https://your-api-host-url.com")).ConnectionLimit = 1;

This won’t cap the speed of a single download, but it prevents overwhelming the server or your network with too many simultaneous transfers.

Key Advantages Over Thread.Sleep

  • Dynamic Adjustments: The throttling stream calculates exact wait times based on real download progress, avoiding over-throttling during slow network spikes or under-throttling during fast bursts.
  • Async-Friendly: Uses Task.Delay instead of Thread.Sleep, so it doesn’t block thread pool threads (critical for async applications).
  • Reusability: The ThrottledStream works with any stream, not just HttpClient responses.

内容的提问来源于stack exchange,提问作者ScottN

火山引擎 最新活动