C# WinForm多线程分块下载超时:Fiddler介入后恢复正常的原因排查
Let’s start with why Fiddler makes your app work—then dig into the bugs in your code that are causing timeouts when Fiddler isn’t running.
How Fiddler Fixes Your Problem
Fiddler acts as a local HTTP proxy, and it silently fixes several issues your code isn’t handling:
- Bypasses .NET’s default connection limit: .NET’s
ServicePointManager.DefaultConnectionLimitdefaults to 2, meaning you can only have 2 concurrent requests to the same server. When you use more chunks than that, requests queue up and time out. Fiddler becomes the target of all your app’s requests, so .NET only limits connections to Fiddler (not the actual download server), and Fiddler manages connections to the server more efficiently. - Adds missing HTTP headers: Fiddler automatically adds standard headers like
User-Agent, which many servers require to serve requests (some will block or throttle requests without it). - Relaxes timeouts: Fiddler’s internal proxy timeouts are more lenient than .NET’s default, and it may automatically retry failed requests that your code would let time out.
Critical Bugs in Your Code
Let’s fix the issues that are causing timeouts without Fiddler:
1. Closure Captures the Wrong Request Instance
In your for loop, you’re capturing the request variable directly in the thread lambda. Closures capture variable references, not values—so all threads end up using the last iteration’s request instance. This means multiple threads are trying to download the same chunk (or invalid ranges), leading to server errors or timeouts.
Fix: Create local variables inside the loop to capture the current iteration’s values:
for (int i = 1; i <= partCount; i++) { // Capture current iteration values in local variables int currentPart = i; long currentStart = startIndex; long currentPartSize = partSize; string currentPartName = $"part-{currentPart}.part"; HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(txtUrl.Text); request.Method = "GET"; // Add timeouts (adjust as needed) request.Timeout = 300000; // 5 minutes request.ReadWriteTimeout = 300000; // Add a valid User-Agent to avoid server blocks request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"; // Fix range calculation: AddRange's end is inclusive, so subtract 1 if (currentPart == partCount) { currentPartSize = totalBytes - currentStart; request.AddRange(currentStart); } else { request.AddRange(currentStart, currentStart + currentPartSize - 1); } startIndex += currentPartSize; downloadThreads[currentPart - 1] = new Thread(() => { // Use local variables here, not loop variables byte[] buffer = new byte[8192]; // Larger buffer = better performance using (FileStream fs = new FileStream(currentPartName, FileMode.Create)) { ucDownloadStatus downloadStatus = new ucDownloadStatus(); downloadStatus.PartName = currentPartName; AddControl(flowDownloadProgress, downloadStatus); try { using (WebResponse response = request.GetResponse()) { using (var responseStream = response.GetResponseStream()) { long totalReceived = 0; int bytesRead; while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0) { fs.Write(buffer, 0, bytesRead); totalReceived += bytesRead; downloadStatus.Progress = (int)(totalReceived * 100 / currentPartSize); } } } } catch (WebException ex) { // Handle errors: show message, delete corrupted chunk MessageBox.Show($"Chunk {currentPartName} failed: {ex.Message}"); if (File.Exists(currentPartName)) File.Delete(currentPartName); } } }); }
2. Low Connection Limit
As mentioned earlier, .NET defaults to 2 concurrent connections per server. Increase this in your form’s constructor to match your chunk count:
public Form1() { InitializeComponent(); nmPartCount.Value = Environment.ProcessorCount; CheckForIllegalCrossThreadCalls = false; // Raise connection limit to match your chunk count (or higher) System.Net.ServicePointManager.DefaultConnectionLimit = (int)nmPartCount.Value; }
3. Broken Chunk Merging
Your JoinParts method has two big issues:
- It doesn’t close
partStream, leading to file locks. - It merges chunks in random order (since
GetFiles()doesn’t guarantee sorting).
Fix:
void JoinParts() { string outputFile = Path.GetFileName(txtUrl.Text); // Sort chunks by their part number string[] sortedParts = Directory.GetFiles(Application.StartupPath, "part-*.part") .OrderBy(file => int.Parse(Path.GetFileNameWithoutExtension(file).Split('-')[1])) .ToArray(); using (FileStream outputStream = new FileStream(outputFile, FileMode.Create)) { foreach (string partFile in sortedParts) { using (FileStream partStream = new FileStream(partFile, FileMode.Open)) { partStream.CopyTo(outputStream); } // Delete the chunk after merging File.Delete(partFile); } } MessageBox.Show("Download completed successfully!"); }
4. Missing Error Handling
Your original code has no try/catch blocks, so you can’t see exactly why timeouts are happening. The added try/catch in the thread code will help you debug specific chunk failures.
Final Notes
Once you fix these issues, your app should work without Fiddler. Fiddler was masking these problems, but now you’ll have a robust, reliable chunked downloader.
内容的提问来源于stack exchange,提问作者Ibrahim Arac

