关于为什么使用单实例,请参考: HttpClient的错误使用
每次使用网络请求时都实例一个HttpClient,业务量小的时候不会存在问题,但是当请求足够大时,按照相关测试 短时间内(两分钟)当请求在3000-5000时请求响应将会下降 会存在大量的网络等待
,当超过5000时会请求错误,显示socket连接耗尽,HttpClient默认释放时间是2-3分钟来着
该请求封装了基本的 异步post、get请求,文件的上传、下载。响应体压缩解码功能
完整代码
public static class HttpRequestHelper
{
// 关于调整的原因,可查看:https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
// 这里将请求设置为全局的单例,这是安全的 因为HttpClient底层是线程安全的
// 设置默认的超时时间,因为是单例 所以不能在执行中修改基础的实例信息
private static readonly HttpClient _client = new HttpClient() { Timeout = TimeSpan.FromSeconds(120) };
///
/// 发送异步post请求
///
///
/// 请不要在请求头中添加 Content-Type
///
///
/// Parameter 参数
///
///
public static async Task<string> HttpPostAsync(string baseUrl, Dictionary<string, string>? headers = null, object? data = null, ContentType contentType = ContentType.Json
, Dictionary<string, string>? parameters = null)
{
// 将参数添加到 URL 中
var url = BuildUrl(baseUrl, parameters);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
if (data != null)
request.Content = BuildContent(data, contentType);
HttpResponseMessage response = await _client.SendAsync(request);
string responseBody = await DeCodeResponse(response);
EnsureSuccessStatus(response, responseBody, "Post请求", url, headers?.ToJson(), data?.ToJson());
return responseBody;
}
///
/// 发送异步GET请求
///
///
///
/// /// 请不要在请求头中添加 Content-Type
///
public static async Task<string> HttpGetAsync(string baseUrl, Dictionary<string, string>? parameters = null, Dictionary<string, string>? headers = null, object? data = null,
ContentType contentType = ContentType.Json)
{
var url = BuildUrl(baseUrl, parameters);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
if (data != null)
request.Content = BuildContent(data, contentType);
var response = await _client.SendAsync(request);
var content = await DeCodeResponse(response);
EnsureSuccessStatus(response, content, "Get请求", url, headers?.ToJson(), data?.ToJson());
return content;
}
///
/// 异步上传文件
///
/// 上传地址
/// 文件地址
/// 请求头
/// query参数
///
public static async Task<string> UploadFileAsync(string baseUrl, string filePath, Dictionary<string, string>? headers = null,
Dictionary<string, string>? parameters = null)
{
// 将参数添加到 URL 中
var url = BuildUrl(baseUrl, parameters);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
// 构建多媒体请求内容
var content = new MultipartFormDataContent();
// 普通文件上传
var fileContent = new StreamContent(File.OpenRead(filePath));
content.Add(fileContent, "file", Path.GetFileName(filePath));
// 读取文件内容并压缩.发现两个问题。如有需要再做压缩
// 1、发现压缩上传会导致进程内存占用非常大 短期内无法释放
// 2、压缩上传的文件 无法正常打开。可能是接收方没有解压缩导致
//string fileContent = File.ReadAllText(filePath);
//byte[] compressedData = CompressString(fileContent);
//var compressedContent = new ByteArrayContent(compressedData);
//content.Add(compressedContent, "file", Path.GetFileName(filePath));
request.Content = content;
// 发送 POST 请求
var response = await _client.SendAsync(request);
//response.EnsureSuccessStatusCode();
string responseBody = await DeCodeResponse(response);
EnsureSuccessStatus(response, responseBody, "文件上传", url, headers?.ToJson());
return responseBody;
}
///
/// 异步下载文件
///
///
///
///
///
///
///
///
///
public static async Task DownloadFileAsync(string url, string savePath, HttpMethod method,
Dictionary<string, object>? headers = null, object? data = null, ContentType contentType = ContentType.Json)
{
using HttpRequestMessage request = new HttpRequestMessage(method, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value.ToString());
}
}
if (data != null)
request.Content = BuildContent(data, contentType);
using var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
EnsureSuccessStatus(response, "", "文件下载", url, headers?.ToJson(), data?.ToJson());
// 获取文件总长度(可选,用于显示进度或检查文件大小)
var totalBytes = response.Content.Headers.ContentLength;
using var streamToReadFrom = await response.Content.ReadAsStreamAsync();
using var streamToWriteTo = File.Create(savePath);
await streamToReadFrom.CopyToAsync(streamToWriteTo);
}
///
/// 验证请求状态
///
///
///
///
///
private static void EnsureSuccessStatus(HttpResponseMessage response, string responseBody, string method, string url, string? header, string? body = null)
{
//response.EnsureSuccessStatusCode(); 检查响应是否是200 若不是 引发异常
// 401 404 状态 应该交给上层业务处理
if (!response.IsSuccessStatusCode
&& ((int)response.StatusCode != 401 && (int)response.StatusCode != 404))
{
Exception exception = new Exception($"header:{header};body:{body};res:{responseBody}");
throw new HttpRequestVerifyException($"{method} {url} ({(int)response.StatusCode}){response.StatusCode}", exception);
}
}
///
/// 解码响应体,如果有编码或压缩的话
///
///
///
private static async Task<string> DeCodeResponse(HttpResponseMessage response)
{
// 解码信息
if (response.Content.Headers != null && response.Content.Headers.ContentEncoding.Contains("gzip"))
{
// 解码
using var gzipStream = new GZipStream(await response.Content.ReadAsStreamAsync(), CompressionMode.Decompress);
using var streamReader = new StreamReader(gzipStream, Encoding.UTF8);
return await streamReader.ReadToEndAsync();
}
else
return await response.Content.ReadAsStringAsync();
}
///
/// 将请求数据绑定到url
///
///
///
///
private static string BuildUrl(string baseUrl, Dictionary<string, string>? parameters)
{
if (parameters == null || parameters.Count == 0)
return baseUrl;
var queryStrings = new List<string>();
foreach (var parameter in parameters)
{
var encodedKey = Uri.EscapeDataString(parameter.Key);
var encodedValue = Uri.EscapeDataString(parameter.Value);
queryStrings.Add($"{encodedKey}={encodedValue}");
}
var queryString = string.Join("&", queryStrings);
return $"{baseUrl}?{queryString}";
}
///
/// 绑定数据到请求体中
///
///
///
///
///
private static HttpContent BuildContent(object data, ContentType contentType)
{
HttpContent content;
if (contentType == ContentType.FormUrlEncoded)
{
var formData = new FormUrlEncodedContent(ObjectToKeyValue(data));
content = formData;
}
else if (contentType == ContentType.Json)
{
//var jsonData = JsonConvert.SerializeObject(data, new JsonSerializerSettings { DateFormatString = "yyyy-MM-dd HH:mm:ss" });
content = new StringContent(data.ToString() ?? "", Encoding.UTF8, "application/json");
}
else if (contentType == ContentType.Xml)
{
content = new StringContent(data.ToString() ?? "", Encoding.UTF8, "text/xml");
}
else if (contentType == ContentType.MultipartFormData)
{
if (data is IDictionary<string, object> formDataDict)
{
var formContent = new MultipartFormDataContent();
foreach (var kvp in formDataDict)
{
var value = kvp.Value?.ToString() ?? "";
formContent.Add(new StringContent(value), kvp.Key);
}
content = formContent;
}
else
{
throw new ArgumentException("formData must be a Dictionary." );
}
}
else
{
throw new ArgumentException("Invalid contentType. Supported values are 'application/x-www-form-urlencoded','application/json','text/xml','multipart/form-data'.");
}
return content;
}
///
/// 压缩文件 返回压缩后的byte
///
///
///
private static byte[] CompressString(string input)
{
using MemoryStream output = new MemoryStream();
using (GZipStream gzipStream = new GZipStream(output, CompressionMode.Compress, leaveOpen: true))
using (StreamWriter writer = new StreamWriter(gzipStream))
{
writer.Write(input);
}
return output.ToArray();
}
private static IEnumerable<KeyValuePair<string, string>> ObjectToKeyValue(object obj)
{
var properties = obj.GetType().GetProperties();
foreach (var prop in properties)
{
var value = prop.GetValue(obj)?.ToString() ?? string.Empty;
yield return new KeyValuePair<string, string>(prop.Name, value);
}
}
}
public enum ContentType
{
Json,
FormUrlEncoded, // application/x-www-form-urlencoded
MultipartFormData,
Xml
}