You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

546 lines
18 KiB

2 years ago
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Text;
  6. using System.Web;
  7. namespace EasyBL
  8. {
  9. public class HttpWebClient
  10. {
  11. private readonly List<HttpUploadingFile> files = new List<HttpUploadingFile>();
  12. private readonly Dictionary<string, string> postingData = new Dictionary<string, string>();
  13. private WebHeaderCollection responseHeaders;
  14. #region events
  15. public event EventHandler<StatusUpdateEventArgs> StatusUpdate;
  16. private void OnStatusUpdate(StatusUpdateEventArgs e)
  17. {
  18. StatusUpdate?.Invoke(this, e);
  19. }
  20. #endregion events
  21. #region properties
  22. /// <summary>
  23. /// 是否自动在不同的请求间保留Cookie, Referer
  24. /// </summary>
  25. public bool KeepContext { get; set; }
  26. /// <summary>
  27. /// 期望的回应的语言
  28. /// </summary>
  29. public string DefaultLanguage { get; set; } = "zh-CN";
  30. /// <summary>
  31. /// GetString()如果不能从HTTP头或Meta标签中获取编码信息,则使用此编码来获取字符串
  32. /// </summary>
  33. public Encoding DefaultEncoding { get; set; } = Encoding.UTF8;
  34. /// <summary>
  35. /// 指示发出Get请求还是Post请求
  36. /// </summary>
  37. public HttpVerb Verb { get; set; } = HttpVerb.GET;
  38. /// <summary>
  39. /// 要上传的文件.如果不为空则自动转为Post请求
  40. /// </summary>
  41. public List<HttpUploadingFile> Files
  42. {
  43. get { return files; }
  44. }
  45. /// <summary>
  46. /// 要发送的Form表单信息
  47. /// </summary>
  48. public Dictionary<string, string> PostingData
  49. {
  50. get { return postingData; }
  51. }
  52. /// <summary>
  53. /// 获取或设置请求资源的地址
  54. /// </summary>
  55. public string Url { get; set; }
  56. /// <summary>
  57. /// 用于在获取回应后,暂时记录回应的HTTP头
  58. /// </summary>
  59. public WebHeaderCollection ResponseHeaders
  60. {
  61. get { return responseHeaders; }
  62. }
  63. /// <summary>
  64. /// 获取或设置期望的资源类型
  65. /// </summary>
  66. public string Accept { get; set; } = "*/*";
  67. /// <summary>
  68. /// 获取或设置请求中的Http头User-Agent的值
  69. /// </summary>
  70. public string UserAgent { get; set; } = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";
  71. /// <summary>
  72. /// 获取或设置Cookie及Referer
  73. /// </summary>
  74. public HttpClientContext Context { get; set; }
  75. /// <summary>
  76. /// 获取或设置获取内容的起始点,用于断点续传,多线程下载等
  77. /// </summary>
  78. public int StartPoint { get; set; }
  79. /// <summary>
  80. /// 获取或设置获取内容的结束点,用于断点续传,多下程下载等. 如果为0,表示获取资源从StartPoint开始的剩余内容
  81. /// </summary>
  82. public int EndPoint { get; set; }
  83. #endregion properties
  84. #region constructors
  85. /// <summary>
  86. /// 构造新的HttpClient实例
  87. /// </summary>
  88. public HttpWebClient()
  89. : this(null)
  90. {
  91. }
  92. /// <summary>
  93. /// 构造新的HttpClient实例
  94. /// </summary>
  95. /// <param name="url">要获取的资源的地址</param>
  96. public HttpWebClient(string url)
  97. : this(url, null)
  98. {
  99. }
  100. /// <summary>
  101. /// 构造新的HttpClient实例
  102. /// </summary>
  103. /// <param name="url">要获取的资源的地址</param>
  104. /// <param name="context">Cookie及Referer</param>
  105. public HttpWebClient(string url, HttpClientContext context)
  106. : this(url, context, false)
  107. {
  108. }
  109. /// <summary>
  110. /// 构造新的HttpClient实例
  111. /// </summary>
  112. /// <param name="url">要获取的资源的地址</param>
  113. /// <param name="context">Cookie及Referer</param>
  114. /// <param name="keepContext">是否自动在不同的请求间保留Cookie, Referer</param>
  115. public HttpWebClient(string url, HttpClientContext context, bool keepContext)
  116. {
  117. this.Url = url;
  118. this.Context = context;
  119. this.KeepContext = keepContext;
  120. if (this.Context == null)
  121. this.Context = new HttpClientContext();
  122. }
  123. #endregion constructors
  124. #region AttachFile
  125. /// <summary>
  126. /// 在请求中添加要上传的文件
  127. /// </summary>
  128. /// <param name="fileName">要上传的文件路径</param>
  129. /// <param name="fieldName">文件字段的名称(相当于&lt;input type=file name=fieldName&gt;)里的fieldName)</param>
  130. public void AttachFile(string fileName, string fieldName)
  131. {
  132. var file = new HttpUploadingFile(fileName, fieldName);
  133. files.Add(file);
  134. }
  135. /// <summary>
  136. /// 在请求中添加要上传的文件
  137. /// </summary>
  138. /// <param name="data">要上传的文件内容</param>
  139. /// <param name="fileName">文件名</param>
  140. /// <param name="fieldName">文件字段的名称(相当于&lt;input type=file name=fieldName&gt;)里的fieldName)</param>
  141. public void AttachFile(byte[] data, string fileName, string fieldName)
  142. {
  143. var file = new HttpUploadingFile(data, fileName, fieldName);
  144. files.Add(file);
  145. }
  146. #endregion AttachFile
  147. /// <summary>
  148. /// 清空PostingData, Files, StartPoint, EndPoint, ResponseHeaders, 并把Verb设置为Get. 在发出一个包含上述信息的请求后,必须调用此方法或手工设置相应属性以使下一次请求不会受到影响.
  149. /// </summary>
  150. public void Reset()
  151. {
  152. Verb = HttpVerb.GET;
  153. files.Clear();
  154. postingData.Clear();
  155. responseHeaders = null;
  156. StartPoint = 0;
  157. EndPoint = 0;
  158. }
  159. private HttpWebRequest CreateRequest()
  160. {
  161. var req = (HttpWebRequest)WebRequest.Create(Url);
  162. req.AllowAutoRedirect = false;
  163. req.CookieContainer = new CookieContainer();
  164. req.Headers.Add("Accept-Language", DefaultLanguage);
  165. req.Accept = Accept;
  166. req.UserAgent = UserAgent;
  167. req.KeepAlive = false;
  168. if (Context.Cookies != null)
  169. req.CookieContainer.Add(Context.Cookies);
  170. if (!string.IsNullOrEmpty(Context.Referer))
  171. req.Referer = Context.Referer;
  172. if (Verb == HttpVerb.HEAD)
  173. {
  174. req.Method = "HEAD";
  175. return req;
  176. }
  177. if (postingData.Count > 0 || files.Count > 0)
  178. Verb = HttpVerb.POST;
  179. if (Verb == HttpVerb.POST)
  180. {
  181. req.Method = "POST";
  182. var memoryStream = new MemoryStream();
  183. using (var writer = new StreamWriter(memoryStream))
  184. {
  185. if (files.Count > 0)
  186. {
  187. const string newLine = "\r\n";
  188. var boundary = Guid.NewGuid().ToString().Replace("-", "");
  189. req.ContentType = "multipart/form-data; boundary=" + boundary;
  190. foreach (string key in postingData.Keys)
  191. {
  192. writer.Write("--" + boundary + newLine);
  193. writer.Write("Content-Disposition: form-data; name=\"{0}\"{1}{1}", key, newLine);
  194. writer.Write(postingData[key] + newLine);
  195. }
  196. foreach (HttpUploadingFile file in files)
  197. {
  198. writer.Write("--" + boundary + newLine);
  199. writer.Write(
  200. "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"{2}",
  201. file.FieldName,
  202. file.FileName,
  203. newLine
  204. );
  205. writer.Write("Content-Type: application/octet-stream" + newLine + newLine);
  206. writer.Flush();
  207. memoryStream.Write(file.Data, 0, file.Data.Length);
  208. writer.Write(newLine);
  209. writer.Write("--" + boundary + newLine);
  210. }
  211. }
  212. else
  213. {
  214. req.ContentType = "application/x-www-form-urlencoded";
  215. var sb = new StringBuilder();
  216. foreach (string key in postingData.Keys)
  217. {
  218. sb.AppendFormat("{0}={1}&", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(postingData[key]));
  219. }
  220. if (sb.Length > 0)
  221. sb.Length--;
  222. writer.Write(sb.ToString());
  223. }
  224. writer.Flush();
  225. using (Stream stream = req.GetRequestStream())
  226. {
  227. memoryStream.WriteTo(stream);
  228. }
  229. }
  230. }
  231. if (StartPoint != 0 && EndPoint != 0)
  232. req.AddRange(StartPoint, EndPoint);
  233. else if (StartPoint != 0 && EndPoint == 0)
  234. req.AddRange(StartPoint);
  235. return req;
  236. }
  237. /// <summary>
  238. /// 发出一次新的请求,并返回获得的回应 调用此方法永远不会触发StatusUpdate事件.
  239. /// </summary>
  240. /// <returns>相应的HttpWebResponse</returns>
  241. public HttpWebResponse GetResponse()
  242. {
  243. var req = CreateRequest();
  244. var res = (HttpWebResponse)req.GetResponse();
  245. responseHeaders = res.Headers;
  246. if (KeepContext)
  247. {
  248. Context.Cookies = res.Cookies;
  249. Context.Referer = Url;
  250. }
  251. return res;
  252. }
  253. /// <summary>
  254. /// 发出一次新的请求,并返回回应内容的流 调用此方法永远不会触发StatusUpdate事件.
  255. /// </summary>
  256. /// <returns>包含回应主体内容的流</returns>
  257. public Stream GetStream()
  258. {
  259. return GetResponse().GetResponseStream();
  260. }
  261. /// <summary>
  262. /// 发出一次新的请求,并以字节数组形式返回回应的内容 调用此方法会触发StatusUpdate事件
  263. /// </summary>
  264. /// <returns>包含回应主体内容的字节数组</returns>
  265. public byte[] GetBytes()
  266. {
  267. var res = GetResponse();
  268. var length = (int)res.ContentLength;
  269. using (var memoryStream = new MemoryStream())
  270. {
  271. var buffer = new byte[0x100];
  272. var rs = res.GetResponseStream();
  273. for (int i = rs.Read(buffer, 0, buffer.Length); i > 0; i = rs.Read(buffer, 0, buffer.Length))
  274. {
  275. memoryStream.Write(buffer, 0, i);
  276. OnStatusUpdate(new StatusUpdateEventArgs((int)memoryStream.Length, length));
  277. }
  278. rs.Close();
  279. return memoryStream.ToArray();
  280. }
  281. }
  282. /// <summary>
  283. /// 发出一次新的请求,以Http头,或Html Meta标签,或DefaultEncoding指示的编码信息对回应主体解码 调用此方法会触发StatusUpdate事件
  284. /// </summary>
  285. /// <returns>解码后的字符串</returns>
  286. public string GetString()
  287. {
  288. var data = GetBytes();
  289. var encodingName = GetEncodingFromHeaders();
  290. if (encodingName == null)
  291. encodingName = GetEncodingFromBody(data);
  292. Encoding encoding;
  293. if (encodingName == null)
  294. encoding = DefaultEncoding;
  295. else
  296. {
  297. try
  298. {
  299. encoding = Encoding.GetEncoding(encodingName);
  300. }
  301. catch (ArgumentException)
  302. {
  303. encoding = DefaultEncoding;
  304. }
  305. }
  306. return encoding.GetString(data);
  307. }
  308. /// <summary>
  309. /// 发出一次新的请求,对回应的主体内容以指定的编码进行解码 调用此方法会触发StatusUpdate事件
  310. /// </summary>
  311. /// <param name="encoding">指定的编码</param>
  312. /// <returns>解码后的字符串</returns>
  313. public string GetString(Encoding encoding)
  314. {
  315. var data = GetBytes();
  316. return encoding.GetString(data);
  317. }
  318. private string GetEncodingFromHeaders()
  319. {
  320. string encoding = null;
  321. var contentType = responseHeaders["Content-Type"];
  322. if (contentType != null)
  323. {
  324. var i = contentType.IndexOf("charset=");
  325. if (i != -1)
  326. {
  327. encoding = contentType.Substring(i + 8);
  328. }
  329. }
  330. return encoding;
  331. }
  332. private static string GetEncodingFromBody(byte[] data)
  333. {
  334. string encodingName = null;
  335. var dataAsAscii = Encoding.ASCII.GetString(data);
  336. if (dataAsAscii != null)
  337. {
  338. var i = dataAsAscii.IndexOf("charset=");
  339. if (i != -1)
  340. {
  341. var j = dataAsAscii.IndexOf("\"", i);
  342. if (j != -1)
  343. {
  344. var k = i + 8;
  345. encodingName = dataAsAscii.Substring(k, (j - k) + 1);
  346. var chArray = new char[2] { '>', '"' };
  347. encodingName = encodingName.TrimEnd(chArray);
  348. }
  349. }
  350. }
  351. return encodingName;
  352. }
  353. /// <summary>
  354. /// 发出一次新的Head请求,获取资源的长度 此请求会忽略PostingData, Files, StartPoint, EndPoint, Verb
  355. /// </summary>
  356. /// <returns>返回的资源长度</returns>
  357. public int HeadContentLength()
  358. {
  359. Reset();
  360. var lastVerb = Verb;
  361. Verb = HttpVerb.HEAD;
  362. using (HttpWebResponse res = GetResponse())
  363. {
  364. Verb = lastVerb;
  365. return (int)res.ContentLength;
  366. }
  367. }
  368. /// <summary>
  369. /// 发出一次新的请求,把回应的主体内容保存到文件 调用此方法会触发StatusUpdate事件 如果指定的文件存在,它会被覆盖
  370. /// </summary>
  371. /// <param name="fileName">要保存的文件路径</param>
  372. public void SaveAsFile(string fileName)
  373. {
  374. SaveAsFile(fileName, FileExistsAction.Overwrite);
  375. }
  376. /// <summary>
  377. /// 发出一次新的请求,把回应的主体内容保存到文件 调用此方法会触发StatusUpdate事件
  378. /// </summary>
  379. /// <param name="fileName">要保存的文件路径</param>
  380. /// <param name="existsAction">指定的文件存在时的选项</param>
  381. /// <returns>是否向目标文件写入了数据</returns>
  382. public bool SaveAsFile(string fileName, FileExistsAction existsAction)
  383. {
  384. var data = GetBytes();
  385. switch (existsAction)
  386. {
  387. case FileExistsAction.Overwrite:
  388. using (BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)))
  389. writer.Write(data);
  390. return true;
  391. case FileExistsAction.Append:
  392. using (BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.Append, FileAccess.Write)))
  393. writer.Write(data);
  394. return true;
  395. default:
  396. if (!File.Exists(fileName))
  397. {
  398. using (
  399. BinaryWriter writer =
  400. new BinaryWriter(new FileStream(fileName, FileMode.Create, FileAccess.Write)))
  401. writer.Write(data);
  402. return true;
  403. }
  404. else
  405. {
  406. return false;
  407. }
  408. }
  409. }
  410. }
  411. public class HttpClientContext
  412. {
  413. public CookieCollection Cookies { get; set; }
  414. public string Referer { get; set; }
  415. }
  416. public enum HttpVerb
  417. {
  418. GET,
  419. POST,
  420. HEAD,
  421. }
  422. public enum FileExistsAction
  423. {
  424. Overwrite,
  425. Append,
  426. Cancel,
  427. }
  428. public class HttpUploadingFile
  429. {
  430. public string FileName { get; set; }
  431. public string FieldName { get; set; }
  432. public byte[] Data { get; set; }
  433. public HttpUploadingFile(string fileName, string fieldName)
  434. {
  435. this.FileName = fileName;
  436. this.FieldName = fieldName;
  437. using (FileStream stream = new FileStream(fileName, FileMode.Open))
  438. {
  439. var inBytes = new byte[stream.Length];
  440. stream.Read(inBytes, 0, inBytes.Length);
  441. Data = inBytes;
  442. }
  443. }
  444. public HttpUploadingFile(byte[] data, string fileName, string fieldName)
  445. {
  446. this.Data = data;
  447. this.FileName = fileName;
  448. this.FieldName = fieldName;
  449. }
  450. }
  451. public class StatusUpdateEventArgs : EventArgs
  452. {
  453. private readonly int bytesGot;
  454. private readonly int bytesTotal;
  455. public StatusUpdateEventArgs(int got, int total)
  456. {
  457. bytesGot = got;
  458. bytesTotal = total;
  459. }
  460. /// <summary>
  461. /// 已经下载的字节数
  462. /// </summary>
  463. public int BytesGot
  464. {
  465. get { return bytesGot; }
  466. }
  467. /// <summary>
  468. /// 资源的总字节数
  469. /// </summary>
  470. public int BytesTotal
  471. {
  472. get { return bytesTotal; }
  473. }
  474. }
  475. }