The operation has timed out 异常,主要原因是 HttpWebRequest 的超时机制配置不当 以及 资源管理存在隐患。
虽然设置了 req.Timeout = 1500(1.5秒),但这对于网络请求来说非常短,且 Timeout属性并不覆盖所有阶段。以下是具体原因分析和修复方案:
- 核心原因分析
A. Timeout 属性覆盖范围有限
HttpWebRequest.Timeout 仅控制 GetResponse() 和 GetRequestStream() 的同步等待时间。它不包含 DNS 解析时间。
DNS 解析延迟:如果 URL 需要 DNS 解析,Windows 系统默认的 DNS 超时可能长达 15 秒。如果 DNS 解析超过 1.5 秒,即使 Timeout 设为 1500,请求也可能在 DNS 阶段卡住直到系统级超时,或者在连接建立初期就报错。
读写超时未设置:Timeout 不控制数据读取过程中的空闲时间。如果服务器接收了请求但发送响应很慢,或者网络波动导致数据包间隔长,可能会触发底层的 Socket 超时,而非 HttpWebRequest 的 Timeout。应配合使用 ReadWriteTimeout。
B. 超时时间过短 (1500ms)
1.5 秒对于包含 DNS 解析、TCP 握手、TLS 协商(如果是 HTTPS)以及服务器处理时间的完整 HTTP 请求来说,通常太短。任何轻微的网络抖动或服务器负载稍高都会导致超时。
C. Expect100Continue 导致的额外往返
默认情况下,HttpWebRequest 会发送 Expect: 100-continue 头。这意味着客户端先发送头部,等待服务器返回 100 Continue 状态码后,再发送 Body 数据。
如果服务器不支持或忽略此头,客户端会等待一段时间(通常 350ms - 1s)后才发送 Body。
在 1500ms 的极短超时限制下,这额外的等待极易导致总耗时超标。
D. 连接池耗尽 (Connection Limit)
HttpWebRequest 默认对同一域名的并发连接数限制为 2 (DefaultConnectionLimit)。
如果之前的请求没有正确关闭 Response 或 Stream,连接会保持在 "Alive" 状态。
当并发请求超过 2 个时,新请求会在队列中等待可用连接。这个等待连接的时间也计入 Timeout。如果前两个请求处理慢,第三个请求可能在拿到连接之前就超时了。
E. 资源未正确释放
代码中 resp 和 stream 没有在 finally 块或 using 语句中确保关闭。如果发生异常,连接可能泄露,进一步加剧连接池耗尽问题。
- 代码修复
using System;
using System.IO;
using System.Net;
using System.Text;
public string DoHttpPost(string url, string postData, string sKey)
{
string result = "";
HttpWebRequest req = null;
HttpWebResponse resp = null;
DateTime stciDT = System.DateTime.Now;
try
{
req = (HttpWebRequest)WebRequest.Create(url);
req.Method = "POST";
// 1. 增加超时时间至合理范围 (例如 5-10 秒),1500ms 太短
req.Timeout = 5000;
// 2. 设置读写超时,防止数据传输过程中卡顿
req.ReadWriteTimeout = 5000;
// 3. 禁用 Expect100Continue,减少一次往返等待,加速请求
req.ServicePoint.Expect100Continue = false;
// 4. 保持连接复用,但确保后续正确关闭
req.KeepAlive = true;
req.ContentType = "application/json";
req.Headers.Add("key", sKey);
byte\[\] data = Encoding.UTF8.GetBytes(postData);
req.ContentLength = data.Length;
// 使用 using 确保请求流正确关闭
using (Stream reqStream = req.GetRequestStream())
{
reqStream.Write(data, 0, data.Length);
} // using 结束会自动 Close 和 Dispose
// 获取响应
resp = (HttpWebResponse)req.GetResponse();
// 使用 using 确保响应流和 StreamReader 正确关闭
using (Stream stream = resp.GetResponseStream())
using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
{
result = reader.ReadToEnd();
}
}
catch (WebException we)
{
// 专门捕获 WebException 以区分超时和其他网络错误
if (we.Status == WebExceptionStatus.Timeout)
{
LogManager.GetLogger().ErrorEx("DoHmCdssHttpPost 超时: " + url);
}
else
{
LogManager.GetLogger().ErrorEx("DoHmCdssHttpPost 网络错误: " + we.Message);
}
}
catch (Exception e)
{
LogManager.GetLogger().ErrorEx("DoHmCdssHttpPost 未知异常: " + e.Message);
}
finally
{
// 5. 确保在 finally 中关闭响应对象,释放连接回连接池
if (resp != null)
{
resp.Close();
}
// 记录耗时
DateTime stciafterDT = System.DateTime.Now;
TimeSpan stciadt = stciafterDT.Subtract(stciDT);
if (!string.IsNullOrEmpty(result))
{
LogManager.GetLogger().InfoEx("DoHmCdssHttpPost-url" + url + "result" + result + "cost" + stciadt.TotalMilliseconds + "");
}
else
{
LogManager.GetLogger().InfoEx("DoHmCdssHttpPost-url" + url + "失败/空结果, 耗时" + stciadt.TotalMilliseconds + "");
}
}
return result;
}
- 关键优化点解释
req.ServicePoint.Expect100Continue = false;
作用:禁止客户端在发送 Body 前等待服务器的 100-Continue 确认。
效果:对于 POST 请求,这能显著减少延迟,特别是在超时时间很短(如 1500ms)的场景下,这是解决"莫名超时"的关键技巧之一。
增加 Timeout 和添加 ReadWriteTimeout
Timeout 建议至少设置为 3000-5000ms,除非在极局域网环境。
ReadWriteTimeout 控制读取响应数据时的超时,防止服务器发送数据慢导致的挂起。
正确的资源释放 (finally + using)
原代码中 resp 没有关闭。如果异常发生,连接不会回到连接池,导致后续请求因等待连接而超时。
finally 块中调用 resp.Close() 是必须的。
关于 DefaultConnectionLimit
如果这是一个高并发场景,建议在应用程序启动时全局设置:
System.Net.ServicePointManager.DefaultConnectionLimit = 50; // 或更高
默认值 2 很容易成为瓶颈。
- 长期建议:迁移到 HttpClient
HttpWebRequest 已被微软标记为过时(Obsolete)。HttpClient 性能更好,默认支持连接池复用,且 API 更现代。
HttpClient 等效写法:
private static readonly HttpClient _httpClient = new HttpClient(); // 单例模式
public async Task<string> DoHmCdssHttpPostAsync(string url, string postData, string sKey)
{
var content = new StringContent(postData, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Add("Huimei_id", sKey);
// 设置超时
_httpClient.Timeout = TimeSpan.FromSeconds(5);
try
{
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// 处理超时
throw new TimeoutException("请求超时");
}
}