HttpClient、WebClient、HttpWebRequest

HttpClient

HttpClient是.NET4.5提供的一个实现了http传输协议的类,该类可以说分装了HttpWebRequest和HttpWebResponse,它可以说是WebClient的精简升级版,适用于新的Metro-Style App以及原生的异步模式,在Metro-Style App中已不能使用原来的WebClient了,所以你可以把它看成是一个替代的类。它与WebClient相比,有几个特点:

  • 一个单一的HttpClient实例,便可以被并发地使用,而不需重新生成实例,换句话说,它是线程安全,而且一次生成,N次使用,中间就少了很多重复的设置过程。
  • HttpClient可以让你实现并插入自己的消息处理,这对于记录以及单元测试非常有好处。
  • HttpClient拥有丰富和扩展性强的Headers和Content类型系统。

当然,HttpClient并非可以完全替代WebClient,因为后者还包括了处理FTP协议的能力,应该说HttpClient主要替代的是在Metro-Style App中WebClient实现Http协议的能力。

下载文件

using System.Net.Http;
/// <summary>
/// 可在整个生存期内实例化一次并重复使用
/// 否则,大量实例化将耗尽重载下的socket套接字数
/// </summary>
static readonly HttpClient _httpClient = new HttpClient();

/// <summary>
/// 下载文件
/// </summary>
/// <param name="url">文件链接</param>
/// <param name="localFile">可存储的本地文件路径</param>
/// <returns></returns>
public async Task DownloadAsync(string url, string localFile)
{
    try
    {
        using (var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
        {
            response.EnsureSuccessStatusCode();
            using (var stream = await response.Content.ReadAsStreamAsync())
            {
                using (var destiStream = File.Open(localFile, FileMode.Create))
                {
                    await stream.CopyToAsync(destiStream);
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

验证大量链接(十万级)是否正常

public async Task<bool> HttpClientGetStreamCheck(string url)
{
    int size = 1000;
    _httpClient.DefaultRequestHeaders.Range = new System.Net.Http.Headers.RangeHeaderValue(0, size);
    var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
    using (var stream = await response.Content.ReadAsStreamAsync())
    {
        var bytes = new byte[size];
        var bytesread = stream.Read(bytes, 0, bytes.Length);
        if (bytesread != size)
        {
            return false;
        }
        stream.Close();
    }
    return true;
}

POST HTTP

private async Task<string> PostInvoke<T>(string url, T body)
{
    var rslt = string.Empty;
    string content = JsonSerializerEx.SerializeWithCamel(body);
    var stringContent = new StringContent(content);
    stringContent.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
    // Headers参数可通过StringContent.Headers.Add()方法添加
    using (var response = await _httpClient.PostAsync(url, stringContent))
    {
        if (response.IsSuccessStatusCode && response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            rslt = await response.Content.ReadAsStringAsync();
        }
    }
    return rslt;
}

Send

var request = GenerateRequestMessage(url, dto);
await _httpClient.SendAsync(request, CancellationToken.None);


private HttpRequestMessage GenerateRequestMessage(string url, object entity)
{
    var request = new HttpRequestMessage(HttpMethod.Post, url);
    request.Headers.Accept.Clear();
    request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    //request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    string json = JsonSerializerEx.SerializeWithCamel(entity);
    request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

    return request;
}

WebClient

上传大文件比如说(300+M)的时候,WebClient将会报内存不足异常(Out of Memory Exceptions),究其原因是因为WebClient方式是一次性将整个文件全部读取到本地内存中,然后再以数据流形式发送至服务器。

string url = @"http://forspeed.onlinedown.net/down/winrar-x64-580sc.exe";
string localFile = "E:\\winrar.exe";
Download(url, localFile);
/// <summary>
/// Http方式下载文件
/// </summary>
/// <param name="url">http地址</param>
/// <param name="localFile">本地文件</param>
/// <returns></returns>
static bool Download(string url, string localFile)
{
    try
    {
        using (WebClient client = new WebClient())
        {
            client.Proxy = null;
            client.DownloadProgressChanged += Client_DownloadProgressChanged;
            client.DownloadFileCompleted += Client_DownloadFileCompleted;
            client.DownloadFileAsync(new Uri(url), localFile);
        }
    }
    catch (Exception ex)
    when (ex.GetType().Name == "WebException")
    {
        WebException we = (WebException)ex;
        using (HttpWebResponse hr = (HttpWebResponse)we.Response)
        {
            int statusCode = (int)hr.StatusCode;
            StringBuilder sb = new StringBuilder();
            StreamReader sr = new StreamReader(hr.GetResponseStream(), Encoding.UTF8);
            sb.Append(sr.ReadToEnd());
            Console.WriteLine(string.Format("下载出现异常!StatusCode:{0},Content: {1}", statusCode, sb));
        }
        return false;
    }
    return true;
}
private static void Client_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
    if (e.Cancelled)
    {
        return;
    }
    Console.WriteLine("下载完成!");
}
private static void Client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
    long iTotalSize = e.TotalBytesToReceive;
    long iSize = e.BytesReceived;
    var percent = Convert.ToDouble(iSize) / Convert.ToDouble(iTotalSize) * 100;
    Console.WriteLine(string.Format("文件大小总共 {1} KB, 当前已接收 {0} KB;\t({2}%)", (iSize / 1024), (iTotalSize / 1024), percent));
}

POST HTTP

private async Task<string> PostInvoke<T>(string url, T body)
{
    var rslt = string.Empty;
    using (var webClient = new WebClient())
    {
        // Headers参数可通过 webClient.Headers.Add()方法添加
        string content = JsonSerializerEx.Serialize(body);
        var data = Encoding.UTF8.GetBytes(content);
        data = await webClient.UploadDataTaskAsync(url, "POST", data);
        rslt = Encoding.UTF8.GetString(data);
    }
    return rslt;
}

HttpWebRequest

下载到缓存

/// <summary>
/// 远程文件下载到内存流
/// </summary>
/// <param name="url">文件链接</param>
/// <returns></returns>
public async Task<Stream> GetStreamAsync(string url)
{
    MemoryStream ms = new MemoryStream();
    try
    {
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        var arrByte = new byte[1024];
        int readCount;
        using (var stream = response.GetResponseStream())
        {
            while ((readCount = await stream.ReadAsync(arrByte, 0, arrByte.Length)) != 0)
            {
                await ms.WriteAsync(arrByte, 0, readCount);
            }
        }
        ms.Seek(0, SeekOrigin.Begin);
    }
    catch (WebException e)
    {
        Console.WriteLine("This program is expected to throw WebException on successful run." +
                                            "\n\nException Message :" + e.Message);
        if (e.Status == WebExceptionStatus.ProtocolError)
        {
            Console.WriteLine("Status Code : {0}", ((HttpWebResponse)e.Response).StatusCode);
            Console.WriteLine("Status Description : {0}", ((HttpWebResponse)e.Response).StatusDescription);
            using (var reader = new StreamReader(e.Response.GetResponseStream()))
            {
                string text = reader.ReadToEnd();
                Console.WriteLine(text);
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }            
    return ms;
}

判断是否支持断点续传

/// <summary>
/// 是否部分响应
/// </summary>
/// <param name="url">服务器链接</param>
/// <returns>True,支持断点续传;False,不支持</returns>
public static bool IsRangeAllowed(string url)
{
    var req = (HttpWebRequest)WebRequest.Create(url);
    // Chrome 伪装浏览器访问
    req.UserAgent = UserAgent;
    req.ServicePoint.ConnectionLimit = 4;
    using (var response = (HttpWebResponse)req.GetResponse())
    {
        if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.PartialContent)
        {
            var dic = GetWebHeaderInfo(response.Headers);
            // Content-Range 服务器支持断点续传
            // Accept-Ranges:bytes 服务器支持按字节下载
            return dic.Any(x => x.Key.ToLower().Contains("range") && x.Value.ToLower().Contains("byte"));
        }
    }
    return false;
}

获取响应头信息

/// <summary>
/// 获取响应头信息
/// </summary>
/// <param name="url">服务器链接</param>
/// <returns></returns>
public static Dictionary<string, string> GetWebHeaderInfo(string url)
{
    var req = (HttpWebRequest)WebRequest.Create(url);
    // Chrome 伪装浏览器访问
    req.UserAgent = UserAgent;
    req.ServicePoint.ConnectionLimit = 4;
    using (var response = (HttpWebResponse)req.GetResponse())
    {
        if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.PartialContent)
        {
            return GetWebHeaderInfo(response.Headers);
        }
    }
    return default;
}
/// <summary>
/// 获取响应头信息
/// </summary>
/// <param name="webHeader">响应头</param>
/// <returns></returns>
public static Dictionary<string, string> GetWebHeaderInfo(WebHeaderCollection webHeader)
{
    if (webHeader == null)
    {
        return default;
    }
    return webHeader.AllKeys.Select((key, i) => new
    {
        HeaderName = key,
        HeaderValue = webHeader[i]
    }).ToDictionary(x => x.HeaderName, x => x.HeaderValue);
}

断点下载

/// <summary>
/// 断点下载
/// </summary>
/// <param name="url">文件链接</param>
/// <param name="localFilePath">存放地址</param>
/// <returns></returns>
static bool Download(string url, string localFilePath)
{
    bool flag;
    // 次下载的文件起始位置
    long startPosition;
    // 写入本地文件流对象
    FileStream fs;
    long remoteFileLength = GetContentLength(url);
    Console.WriteLine("remoteFileLength: " + remoteFileLength);
    if (remoteFileLength == 745)
    {
        Console.WriteLine("远程文件不存在!");
        return false;
    }
    // 判断要下载的文件是否存在 断点续传
    if (File.Exists(localFilePath))
    {
        fs = File.OpenWrite(localFilePath);
        // 获取已经下载的长度
        startPosition = fs.Length;
        if (startPosition >= remoteFileLength)
        {
            Console.WriteLine($"获取已经下载的长度{startPosition}经大于等于远程文件长度{remoteFileLength}");
            fs.Close();
            return true;
        }
        else
        {
            fs.Seek(startPosition, SeekOrigin.Current);
        }
    }
    else
    {
        fs = new FileStream(localFilePath, FileMode.Create);
        startPosition = 0;
    }
    try
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
        if (startPosition > 0)
        {
            // 定义远程文件读取位置
            request.AddRange((int)startPosition);
        }
        // 向服务器请求,获得服务器的回应数据流
        using (Stream stream = request.GetResponse().GetResponseStream())
        {
            byte[] arrByte = new byte[512];
            // 向远程文件读取字节流
            int contentSize = stream.Read(arrByte, 0, arrByte.Length);
            long curPos = startPosition;
            // 如果读取长度大于零则继续读
            while (contentSize > 0)
            {
                curPos += contentSize;
                int percent = (int)(curPos * 100 / remoteFileLength);
                Console.WriteLine($"percent={percent}%");
                // 写入本地文件
                fs.Write(arrByte, 0, contentSize);
                contentSize = stream.Read(arrByte, 0, arrByte.Length);
            }
        }
        flag = true;

    }
    catch (Exception ex)
    {
        string err = ex.Message;
        Console.WriteLine(err);
        flag = false;
    }
    finally
    {
        // 关闭流
        fs.Close();
    }
    return flag;
}

多任务分块下载器

取得远程文件长度

/// <summary>
/// 取得远程文件长度
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private static long GetContentLength(string url)
{
    long length = 0;
    try
    {
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
        req.Method = "HEAD";    // 获取头部信息
        //req.Timeout = 1000;
        using (HttpWebResponse response = (HttpWebResponse)req.GetResponse())
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                length = response.ContentLength;
            }
        }
        // 若网站设置了反爬虫,或是GZip压缩格式的,或文件太大无法确定消息的大小
        if (length == -1)
        {
            req = (HttpWebRequest)WebRequest.Create(url);
            // Chrome 伪装浏览器访问
            req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36";
            req.ServicePoint.ConnectionLimit = 4;
            using (HttpWebResponse response = (HttpWebResponse)req.GetResponse())
            {
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    length = response.ContentLength;
                }
            }
        }
        return length;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    return length;
}

取得远程文件的名称

/// <summary>
/// 取得远程文件的名称
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private static string GetRemoteName(string url)
{
    string rlst = string.Empty;
    try
    {
        var uri = new Uri(url);
        var req = (HttpWebRequest)WebRequest.CreateDefault(uri);
        req.Method = "HEAD";
        //req.Timeout = 1000;
        using (var response = (HttpWebResponse)req.GetResponse())
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (uri.Equals(response.ResponseUri))   // 未重定向
                {
                    rlst = Path.GetFileName(url);
                }
                else
                {
                    rlst = response.Headers["Content-Disposition"]; // attachment;filename=**
                    if (string.IsNullOrEmpty(rlst))
                    {
                        rlst = response.ResponseUri.Segments[response.ResponseUri.Segments.Length - 1];
                    }
                    else
                    {
                        rlst = rlst.Remove(0, rlst.IndexOf("filename=") + 9);
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    // 防止出现乱码
    rslt = System.Web.HttpUtility.UrlDecode(rslt, System.Text.Encoding.UTF8);
    // 防止名字过长
    if (rslt.Length > 260)
    {
         rslt = rslt.Substring(rslt.Length - 260);
    }
    return rlst;
}

传递参数

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

public class HttpHelper
{
    /// <summary>
    /// 异步POST请求
    /// </summary>
    /// <typeparam name="TResponse">响应类型</typeparam>
    /// <param name="webApiUrl">资源URL</param>
    /// <param name="param">请求参数对象</param>
    /// <param name="timeout">请求超时(ms)</param>
    /// <param name="contentType">请求的Content-Type</param>
    /// <returns></returns>
    public async Task<TResponse> GetAsync<TResponse>(string webApiUrl, object param, int timeout = 1_000, string contentType = "application/json") where TResponse : class
    {
        return await ImplAsync<TResponse>(webApiUrl, param, "GET", contentType, timeout);
    }
    /// <summary>
    /// 异步GET请求
    /// </summary>
    /// <typeparam name="TResponse"></typeparam>
    /// <param name="webApiUrl"></param>
    /// <param name="param"></param>
    /// <param name="timeout"></param>
    /// <param name="contentType"></param>
    /// <returns></returns>
    public async Task<TResponse> PostAsync<TResponse>(string webApiUrl, object param, int timeout = 1_000, string contentType = "application/json") where TResponse : class
    {
        return await ImplAsync<TResponse>(webApiUrl, param, "POST", contentType, timeout);
    }

    private async Task<TResponse>  ImplAsync<TResponse>(string webApiUrl, object param, string httpVerb, string contentType = "application/json", int timeout = 1_000) where TResponse : class
    {
        var req = CreateWebRequest(webApiUrl, param, httpVerb, contentType, timeout);
        var response = await InvokeWebRequestAsync(req);
        if (typeof(TResponse) == typeof(string))
        {
            return response.Value<TResponse>();
        }
        return JsonConvert.DeserializeObject<TResponse>(response);
    }

    // TODO
}

HttpHelper helper = new HttpHelper();
var str = await helper.GetAsync("http://www.baidu.com", null);

HTTP一般执行过程

/// <summary>
/// HTTP初始化,构造HttpWebRequest
/// </summary>
/// <typeparam name="T">body数据类型</typeparam>
/// <param name="webApiUrl">资源URL</param>
/// <param name="body">body数据</param>
/// <param name="requestMethod">请求的方法(POST、DELETE、PUT、GET)</param>
/// <param name="contentType">Content-Type类型</param>
/// <param name="timeOut">超时时间(ms)</param>
/// <returns></returns>
private HttpWebRequest CreateWebRequest<T>(string webApiUrl, T body, string requestMethod, string contentType = "application/json", int timeOut = 10_000) where T : class
{
    var rslt = CreateWebRequest(webApiUrl, requestMethod, contentType, timeOut);
    if (requestMethod.ToUpper() == "GET")
    {
        return rslt;
    }
    // params参数可直接跟在请求的URL地址后面
    if (body != null)
    {
        string content = JsonConvert.SerializeObject(body);
        var bytes = Encoding.UTF8.GetBytes(content);
        var stream = rslt.GetRequestStream();
        stream.Write(bytes, 0, bytes.Length);
        stream.Close();
    }
    return rslt;
}
/// <summary>
/// HTTP初始化,构造HttpWebRequest
/// </summary>
/// <param name="webApiUrl">资源URL</param>
/// <param name="requestMethod">请求的方法(POST、DELETE、PUT、GET)</param>
/// <param name="contentType">Content-Type类型</param>
/// <param name="timeOut">超时时间(ms)</param>
/// <returns></returns>
private HttpWebRequest CreateWebRequest(string webApiUrl, string requestMethod = "GET", string contentType = "application/json", int timeOut = 10_000)
{
    var rslt = (HttpWebRequest)WebRequest.Create(webApiUrl);
    rslt.Method = requestMethod;
    rslt.ContentType = contentType;
    rslt.KeepAlive = true;
    // params参数可直接跟在请求的URL地址后面
    // Headers参数可通过request.Headers.Add()方法添加 token等
    // 获取设置身份认证及请求超时时间
    rslt.Credentials = CredentialCache.DefaultCredentials;
    rslt.Timeout = timeOut;

    return rslt;
}
/// <summary>
/// 执行HTTP,返回响应内容
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<string> InvokeWebRequestAsync(HttpWebRequest request)
{
    using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
    {
        var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
        return await reader.ReadToEndAsync();
    }
}

HTTP 签名+认证

  1. 确保来源准确、中间无篡改,对参数进行签名,并在参数后面追加一个签名字段;

形如:http://xxx/api/third/method?para1=value1&para2=value2&sign=signValue

  1. 确保当前调用有权限,可以把token、用户名与密码也加到签名里,具体的签名规则可以双方协定,
    比如对所有待签名参数可以按参数名称递增排序;签名使用SHA1;签名结果大写表示;
  2. 用户名与密码也可以加到Headers里面;
private async Task<XXXRepsonseDto> MethodAsync(string url, string token, XXXRequestDto dtoRequest, DtoConfig config)
{
    var innerAccess = new InnerAccessAuthorization();
    innerAccess.App_id = config.App_id;
    innerAccess.Sercret = config.Sercret;
    
    // 签名
    var dicInnerAccess = innerAccess.ToDicDescValue();
    var uploadDataDto = new UploadDataRequestDto
    {
        Token = token,
        Timestamp = WSCommFunc.GenerateTimestamp(),
    };
    var dic = uploadDataDto.ToDicDescValue();
    var pairs = WSCommFunc.GenerateParam(dic, dicInnerAccess, config);
    url = WSCommFunc.ConcatUrl(url, pairs.Value, pairs.Key);

    // 接口调用
    _logger.LogInformation($"MethodAsync request:{JsonSerializer.Serialize(dtoRequest)}");
    var api = new HttpHelper();
    var request = api.CreateWebRequest(url, dtoRequest, "POST");
    request.Headers.Add("Inner-Access-Authorization", JsonSerializerEx.SerializeWithCamel(innerAccess));
    var content = await api.InvokeWebRequestAsync(request);
    _logger.LogInformation($"MethodAsync response:{content}");
    var dto = JsonSerializerEx.DeserializeWithCamel<HandlerResult<UploadDataRepsonseDto>>(content);
    // 验证结果
    if(dto.State == "10000")
    {
        _logger.LogInformation($"MethodAsync 返回正常--{JsonSerializer.Serialize(dto)},request:{JsonSerializer.Serialize(dtoRequest)}");
    }
    else
    {
        _logger.LogError($"MethodAsync 返回报错--{JsonSerializer.Serialize(dto)},url:{url},request:{JsonSerializer.Serialize(dtoRequest)}");
        return null;
    }
    return dto.Value;

}
/// <summary>
/// 内部访问授权类
/// </summary>
internal class InnerAccessAuthorization
{
    /// <summary>
    /// 第三方平台身份
    /// </summary>
    [Description("app_id")]
    public string App_id { get; set; } 
    /// <summary>
    /// 版本
    /// </summary>
    [Description("version")]
    public string Version { get; set; } = "1.0";
    /// <summary>
    /// 接口签名安全认证
    /// </summary>
    public string Sercret = ""; 
}
posted @ 2019-12-31 20:03  wesson2019  阅读(510)  评论(0编辑  收藏  举报