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; ({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." +
"
Exception 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>
/// <param name="localFilePath">存放地址</param>
/// <returns></returns>
static bool Download(string url, string localFilePath)
{
bool flag;
// 次下载的文件起始位置
long startPosition;
// 写入本地文件流对象
FileStream fs;
long remoteFileLength = GetHttpLength(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 GetHttpLength(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;
}
}
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);
}
return rlst;
}
传递参数
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
private async Task<TResponse> Post<TResponse, TRequest>(string webApiUrl, TRequest param, string contentType = "application/json") where TResponse : class where TRequest : class
{
try
{
string body = JsonSerializer.Serialize(param); // 或 Newtonsoft.Json
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(webApiUrl);
req.Method = "POST";
req.ContentType = contentType;
// params参数可直接跟在请求的URL地址后面
// Headers参数可通过request.Headers.Add()方法添加
var bytes = Encoding.UTF8.GetBytes(body);
var stream = req.GetRequestStream();
stream.Write(bytes, 0, bytes.Length);
stream.Close();
// 获取设置身份认证及请求超时时间
req.Credentials = CredentialCache.DefaultCredentials;
req.Timeout = 1_000;
using (HttpWebResponse response = (HttpWebResponse)req.GetResponse())
{
var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
var rsltContent = await reader.ReadToEndAsync();
return JsonSerializer.Deserialize<TResponse>(rsltContent);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return 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;
}
string content = JsonSerializerEx.Serialize(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 签名+认证
- 确保来源准确、中间无篡改,对参数进行签名,并在参数后面追加一个签名字段;
形如:http://xxx/api/third/method?para1=value1¶2=value2&sign=signValue
- 确保当前调用有权限,可以把token、用户名与密码也加到签名里,具体的签名规则可以双方协定,
比如对所有待签名参数可以按参数名称递增排序;签名使用SHA1;签名结果大写表示; - 用户名与密码也可以加到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 = "";
}