|
|
using Apewer; using Apewer.Internals; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Security; using System.Security; using System.Security.Cryptography.X509Certificates; using System.Text;
namespace Apewer.Network {
/// <summary></summary>
public class HttpClient {
string _key = TextUtility.Key(); int _block = 1024;
/// <summary></summary>
public string Key { get => _key; }
/// <summary>获取进度的块大小,以字节为单位,默认值:1024(1KB)。</summary>
public int Block { get => _block; set => NumberUtility.Restrict(value, 1, 1048576); }
/// <summary>构建 <see cref="HttpClient"/> 的实例。</summary>
public HttpClient() { RequestHeaders = new StringPairs(); }
#region request
/// <summary>获取或设置将要请求的方法,默认为 GET。</summary>
public HttpMethod Method { get; set; }
/// <summary>获取或设置将要请求的地址。</summary>
public string Url { get; set; }
/// <summary>获取或设置请求主体的超时毫秒数,默认值取决于运行时(100,000 毫秒)。</summary>
public int Timeout { get; set; }
/// <summary>获取或设置请求使用的证书。</summary>
public X509Certificate ClientCertificate { get; set; }
/// <summary>获取或设置请求的自定义头。</summary>
public StringPairs RequestHeaders { get; set; }
/// <summary>获取或设置请求的 Cookies。</summary>
public Cookie[] RequestCookies { get; set; }
/// <summary>获取或设置 POST 请求发送的数据。当 Stream 属性有效时此属性将被忽略。</summary>
public byte[] RequestData { get; set; }
/// <summary>获取或设置 POST 请求发送的数据。此属性有效时将忽略 Data 属性。</summary>
public Stream RequestStream { get; set; }
/// <summary>获取或设置写入请求主体的进度回调。</summary>
public Action<long> RequestProgress { get; set; }
/// <summary>获取或设置请求的内容类型,默认为空。此属性可能会被自定义头取代。</summary>
public string RequestContentType { get; set; }
/// <summary>获取或设置请求的内容字节长度,此属性仅在指定大于 0 时被使用。此属性可能会被自定义头或主体字节数组取代。</summary>
public long RequestContentLength { get; set; }
/// <summary>获取或设置用户代理。此属性可能会被自定义头取代。</summary>
public string UserAgent { get; set; }
/// <summary>获取或设置来源。此属性可能会被自定义头取代。</summary>
public string Referer { get; set; }
#endregion
#region response
/// <summary>跟随响应的重定向。</summary>
/// <remarks>默认值:False</remarks>
public bool AllowRedirect { get; set; }
/// <summary>获取或设置要接收响应主体的流。默认为 NULL 值。</summary>
/// <remarks>指定为 NULL 时,响应体将写入字节数组;<br />非 NULL 时,响应体将写入此流,并忽略 ResponseData 属性。</remarks>
public Stream ResponseStream { get; set; }
/// <summary>获取或设置读取响应主体的进度回调</summary>
public Action<long> ResponseProgress { get; set; }
/// <summary>获取响应的状态。</summary>
public string ResponseStatus { get; private set; }
/// <summary>获取响应的缓存状态。</summary>
public bool ResponseCached { get; private set; }
/// <summary>获取响应的头。</summary>
public Cookie[] ResponseCookies { get; private set; }
/// <summary>获取响应的头。</summary>
public StringPairs ResponseHeaders { get; private set; }
/// <summary>获取响应的主体,当指定流时此属性将不被使用。</summary>
public byte[] ResponseData { get; private set; }
#endregion
#region send
object _locker = new object(); Exception _exception = null; HttpWebRequest _request = null; HttpWebResponse _response = null;
Nullable<DateTime> _request_time = null; Nullable<DateTime> _response_time = null;
/// <summary>发送请求的时间。</summary>
public Nullable<DateTime> RequestTime { get => _request_time; }
/// <summary>接收响应的时间。</summary>
public Nullable<DateTime> ResponseTime { get => _response_time; }
/// <summary>获取最近发生的异常。</summary>
public Exception Exception { get { return _exception; } }
Exception Return(Exception ex) { _exception = ex; return ex; }
/// <summary>发送请求,并获取响应。</summary>
/// <param name="catchEx">捕获发生的异常,将异常作为返回值。</param>
/// <returns>发生的异常。</returns>
public Exception Send(bool catchEx = true) { lock (_locker) { _exception = null;
ResponseStatus = default; ResponseCached = default; ResponseCookies = default; ResponseHeaders = default; ResponseData = default;
var url = Url; if (url.IsEmpty()) { var ex = new MissingMemberException("未指定 URL。"); if (!catchEx) throw ex; return Return(ex); }
var method = Method; if (method == HttpMethod.NULL) { if (RequestData != null || RequestStream != null) method = HttpMethod.POST; else method = HttpMethod.GET; }
if (catchEx) { try { _request = Prepare(url, method); _request_time = DateTime.Now; _response = _request.GetResponse() as HttpWebResponse; _response_time = DateTime.Now; Parse(_response); } catch (Exception ex) { _response_time = DateTime.Now; _exception = ex; if (ex is WebException webEx) Parse((HttpWebResponse)webEx.Response); return ex; } } else { _request = Prepare(url, method); _request_time = DateTime.Now; _response = _request.GetResponse() as HttpWebResponse; _response_time = DateTime.Now; Parse(_response); } }
return null; }
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.InvalidOperationException"></exception>
/// <exception cref="System.NotSupportedException"></exception>
/// <exception cref="System.ObjectDisposedException"></exception>
/// <exception cref="System.Security.SecurityException"></exception>
/// <exception cref="System.Net.ProtocolViolationException"></exception>
/// <exception cref="System.Net.WebException"></exception>
HttpWebRequest Prepare(string url, HttpMethod method) { var https = url.ToLower().StartsWith("https"); if (https) SslUtility.ApproveValidation();
var request = (HttpWebRequest)WebRequest.Create(url);
var certificate = ClientCertificate; if (certificate != null) request.ClientCertificates.Add(certificate);
var timeout = Timeout; if (timeout > 0) request.Timeout = timeout; request.Method = method.ToString(); request.AllowAutoRedirect = AllowRedirect;
request.CookieContainer = new CookieContainer(); var cookies = RequestCookies; if (cookies != null) { foreach (var cookie in cookies) { if (cookie == null) continue; request.CookieContainer.Add(cookie); } }
var length = RequestContentLength; var type = RequestContentType; var ua = UserAgent; var r = Referer; var headers = RequestHeaders; if (headers != null) foreach (var header in headers) { var key = header.Key.ToTrim(); ; var value = header.Value.ToTrim(); if (TextUtility.IsBlank(key)) continue; if (TextUtility.IsBlank(value)) continue; try { switch (key.Lower()) { case "accept": request.Accept = value; break; case "connection": request.Connection = value; break; case "content-length": case "contentlength": length = Math.Max(length, NumberUtility.Int64(value)); break; case "content-type": case "contenttype": type = value; break; case "expect": request.Expect = value; break; #if !NET20
case "host": request.Host = value; break; #endif
case "if-modified-since": var requestIfModifiedSince = ClockUtility.Parse(value); if (requestIfModifiedSince != null) request.IfModifiedSince = requestIfModifiedSince.Value; break; case "referer": case "referrer": r = value; break; case "transfer-encoding": request.TransferEncoding = value; break; case "user-agent": case "useragent": ua = value; break; default: request.Headers.Add(header.Key, header.Value); break; } } catch { } } if (length > 0L) request.ContentLength = length; if (type.NotEmpty()) request.ContentType = type; if (ua.NotEmpty()) request.UserAgent = ua; if (r.NotEmpty()) request.Referer = r;
if (method == HttpMethod.POST) { var stream = RequestStream; var data = RequestData; if (stream != null) { var body = request.GetRequestStream(); BytesUtility.Read(stream, body, RequestProgress, Block); } else if (data != null) { request.ContentLength = data.LongLength; var body = request.GetRequestStream(); BytesUtility.Write(body, data, RequestProgress, Block); } }
return request; }
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.InvalidOperationException"></exception>
/// <exception cref="System.NotSupportedException"></exception>
/// <exception cref="System.ObjectDisposedException"></exception>
/// <exception cref="System.Net.ProtocolViolationException"></exception>
/// <exception cref="System.Net.WebException"></exception>
void Parse(HttpWebResponse response) { if (response == null) return;
ResponseStatus = ((int)response.StatusCode).ToString(); ResponseCached = response.IsFromCache;
var headers = new StringPairs(); foreach (var key in response.Headers.AllKeys) { var values = response.Headers.GetValues(key); headers.Add(key, values.Join(",")); } ResponseHeaders = headers;
var cb = new ArrayBuilder<Cookie>(); foreach (var item in response.Cookies) { var cookie = item as Cookie; if (cookie == null) continue; cb.Add(cookie); }
var body = response.GetResponseStream(); var progress = ResponseProgress; var hasProgress = progress != null; var stream = ResponseStream; if (stream == null) ResponseData = BytesUtility.Read(body); else BytesUtility.Read(body, stream, progress, Block);
response.Close(); }
#endregion
#region static
const int SimpleTimeout = 30000;
/// <summary>GET</summary>
public static HttpClient Get(string url, int timeout = 30000) { var http = new HttpClient(); http.Url = url; http.Timeout = timeout; http.Method = HttpMethod.GET; http.Send(); return http; }
/// <summary>POST</summary>
public static HttpClient Post(string url, byte[] data, int timeout = 30000, string type = "application/octet-stream") { var http = new HttpClient(); http.Url = url; http.Timeout = timeout; http.Method = HttpMethod.POST; if (!string.IsNullOrEmpty(type)) http.RequestContentType = type; if (data != null) { http.RequestContentLength = data.LongLength; http.RequestData = data; } http.Send(); return http; }
/// <summary>POST text/plain</summary>
public static HttpClient Text(string url, string text, int timeout = 30000, string type = "text/plain") { return Post(url, TextUtility.Bytes(text), timeout, type); }
/// <summary>POST application/x-www-form-urlencoded</summary>
public static HttpClient Form(string url, IDictionary<string, string> form, int timeout = 30000) { if (form == null) return Post(url, BytesUtility.Empty, timeout, "application/x-www-form-urlencoded");
var cache = new List<string>(); foreach (var i in form) { var key = TextUtility.EncodeUrl(i.Key); var value = TextUtility.EncodeUrl(i.Value); cache.Add(key + "=" + value); } var text = string.Join("&", cache.ToArray()); var data = TextUtility.Bytes(text); return Post(url, data, timeout, "application/x-www-form-urlencoded"); }
/// <summary>POST application/x-www-form-urlencoded</summary>
public static HttpClient Form(string url, Dictionary<string, string> form, int timeout = 30000) => Form(url, form as IDictionary<string, string>, timeout);
/// <summary>合并表单参数,不包含 Query 的 ? 符号。</summary>
public static string MergeForm(Dictionary<string, string> form) { if (form == null) return ""; if (form.Count < 1) return ""; var cache = new List<string>(); foreach (var i in form) { var key = TextUtility.EncodeUrl(i.Key); var value = TextUtility.EncodeUrl(i.Value); cache.Add(key + "=" + value); } var text = string.Join("&", cache.ToArray()); return text; }
#endregion
}
}
|