C#网络请求封装,HttpClient 静态单实例

关于为什么使用单实例,请参考: 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
}

你可能感兴趣的:(后端技术,记录,c#,网络,后端,asp.net,.netcore)