一個C#的與web服務器交互的HttpClient類

openkk 12年前發布 | 94K 次閱讀 C# 網絡工具包

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Net;
using System.Web;

namespace Deerchao.Utility { public class HttpClient {

    #region fields
    private bool keepContext;
    private string defaultLanguage = "zh-CN";
    private Encoding defaultEncoding = Encoding.UTF8;
    private string accept = "*/*";
    private string userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";
    private HttpVerb verb = HttpVerb.GET;
    private HttpClientContext context;
    private readonly List<HttpUploadingFile> files = new List<HttpUploadingFile>();
    private readonly Dictionary<string, string> postingData = new Dictionary<string, string>();
    private string url;
    private WebHeaderCollection responseHeaders;
    private int startPoint;
    private int endPoint;
    #endregion

    #region events
    public event EventHandler<StatusUpdateEventArgs> StatusUpdate;

    private void OnStatusUpdate(StatusUpdateEventArgs e)
    {
        EventHandler<StatusUpdateEventArgs> temp = StatusUpdate;

        if (temp != null)
            temp(this, e);
    }
    #endregion

    #region properties
    /// <summary>
    /// 是否自動在不同的請求間保留Cookie, Referer
    /// </summary>
    public bool KeepContext
    {
        get { return keepContext; }
        set { keepContext = value; }
    }

    /// <summary>
    /// 期望的回應的語言
    /// </summary>
    public string DefaultLanguage
    {
        get { return defaultLanguage; }
        set { defaultLanguage = value; }
    }

    /// <summary>
    /// GetString()如果不能從HTTP頭或Meta標簽中獲取編碼信息,則使用此編碼來獲取字符串
    /// </summary>
    public Encoding DefaultEncoding
    {
        get { return defaultEncoding; }
        set { defaultEncoding = value; }
    }

    /// <summary>
    /// 指示發出Get請求還是Post請求
    /// </summary>
    public HttpVerb Verb
    {
        get { return verb; }
        set { verb = value; }
    }

    /// <summary>
    /// 要上傳的文件.如果不為空則自動轉為Post請求
    /// </summary>
    public List<HttpUploadingFile> Files
    {
        get { return files; }
    }

    /// <summary>
    /// 要發送的Form表單信息
    /// </summary>
    public Dictionary<string, string> PostingData
    {
        get { return postingData; }
    }

    /// <summary>
    /// 獲取或設置請求資源的地址
    /// </summary>
    public string Url
    {
        get { return url; }
        set { url = value; }
    }

    /// <summary>
    /// 用于在獲取回應后,暫時記錄回應的HTTP頭
    /// </summary>
    public WebHeaderCollection ResponseHeaders
    {
        get { return responseHeaders; }
    }

    /// <summary>
    /// 獲取或設置期望的資源類型
    /// </summary>
    public string Accept
    {
        get { return accept; }
        set { accept = value; }
    }

    /// <summary>
    /// 獲取或設置請求中的Http頭User-Agent的值
    /// </summary>
    public string UserAgent
    {
        get { return userAgent; }
        set { userAgent = value; }
    }

    /// <summary>
    /// 獲取或設置Cookie及Referer
    /// </summary>
    public HttpClientContext Context
    {
        get { return context; }
        set { context = value; }
    }

    /// <summary>
    /// 獲取或設置獲取內容的起始點,用于斷點續傳,多線程下載等
    /// </summary>
    public int StartPoint
    {
        get { return startPoint; }
        set { startPoint = value; }
    }

    /// <summary>
    /// 獲取或設置獲取內容的結束點,用于斷點續傳,多下程下載等.
    /// 如果為0,表示獲取資源從StartPoint開始的剩余內容
    /// </summary>
    public int EndPoint
    {
        get { return endPoint; }
        set { endPoint = value; }
    }

    #endregion

    #region constructors
    /// <summary>
    /// 構造新的HttpClient實例
    /// </summary>
    public HttpClient()
        : this(null)
    {
    }

    /// <summary>
    /// 構造新的HttpClient實例
    /// </summary>
    /// <param name="url">要獲取的資源的地址</param>
    public HttpClient(string url)
        : this(url, null)
    {
    }

    /// <summary>
    /// 構造新的HttpClient實例
    /// </summary>
    /// <param name="url">要獲取的資源的地址</param>
    /// <param name="context">Cookie及Referer</param>
    public HttpClient(string url, HttpClientContext context)
        : this(url, context, false)
    {
    }

    /// <summary>
    /// 構造新的HttpClient實例
    /// </summary>
    /// <param name="url">要獲取的資源的地址</param>
    /// <param name="context">Cookie及Referer</param>
    /// <param name="keepContext">是否自動在不同的請求間保留Cookie, Referer</param>
    public HttpClient(string url, HttpClientContext context, bool keepContext)
    {
        this.url = url;
        this.context = context;
        this.keepContext = keepContext;
        if (this.context == null)
            this.context = new HttpClientContext();
    }
    #endregion

    #region AttachFile
    /// <summary>
    /// 在請求中添加要上傳的文件
    /// </summary>
    /// <param name="fileName">要上傳的文件路徑</param>
    /// <param name="fieldName">文件字段的名稱(相當于&lt;input type=file name=fieldName&gt;)里的fieldName)</param>
    public void AttachFile(string fileName, string fieldName)
    {
        HttpUploadingFile file = new HttpUploadingFile(fileName, fieldName);
        files.Add(file);
    }

    /// <summary>
    /// 在請求中添加要上傳的文件
    /// </summary>
    /// <param name="data">要上傳的文件內容</param>
    /// <param name="fileName">文件名</param>
    /// <param name="fieldName">文件字段的名稱(相當于&lt;input type=file name=fieldName&gt;)里的fieldName)</param>
    public void AttachFile(byte[] data, string fileName, string fieldName)
    {
        HttpUploadingFile file = new HttpUploadingFile(data, fileName, fieldName);
        files.Add(file);
    }
    #endregion

    /// <summary>
    /// 清空PostingData, Files, StartPoint, EndPoint, ResponseHeaders, 并把Verb設置為Get.
    /// 在發出一個包含上述信息的請求后,必須調用此方法或手工設置相應屬性以使下一次請求不會受到影響.
    /// </summary>
    public void Reset()
    {
        verb = HttpVerb.GET;
        files.Clear();
        postingData.Clear();
        responseHeaders = null;
        startPoint = 0;
        endPoint = 0;
    }

    private HttpWebRequest CreateRequest()
    {
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
        req.AllowAutoRedirect = false;
        req.CookieContainer = new CookieContainer();
        req.Headers.Add("Accept-Language", defaultLanguage);
        req.Accept = accept;
        req.UserAgent = userAgent;
        req.KeepAlive = false;

        if (context.Cookies != null)
            req.CookieContainer.Add(context.Cookies);
        if (!string.IsNullOrEmpty(context.Referer))
            req.Referer = context.Referer;

        if (verb == HttpVerb.HEAD)
        {
            req.Method = "HEAD";
            return req;
        }

        if (postingData.Count > 0 || files.Count > 0)
            verb = HttpVerb.POST;

        if (verb == HttpVerb.POST)
        {
            req.Method = "POST";

            MemoryStream memoryStream = new MemoryStream();
            StreamWriter writer = new StreamWriter(memoryStream);

            if (files.Count > 0)
            {
                string newLine = "\r\n";
                string boundary = Guid.NewGuid().ToString().Replace("-", "");
                req.ContentType = "multipart/form-data; boundary=" + boundary;

                foreach (string key in postingData.Keys)
                {
                    writer.Write("--" + boundary + newLine);
                    writer.Write("Content-Disposition: form-data; name=\"{0}\"{1}{1}", key, newLine);
                    writer.Write(postingData[key] + newLine);
                }

                foreach (HttpUploadingFile file in files)
                {
                    writer.Write("--" + boundary + newLine);
                    writer.Write(
                        "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"{2}",
                        file.FieldName,
                        file.FileName,
                        newLine
                        );
                    writer.Write("Content-Type: application/octet-stream" + newLine + newLine);
                    writer.Flush();
                    memoryStream.Write(file.Data, 0, file.Data.Length);
                    writer.Write(newLine);
                    writer.Write("--" + boundary + newLine);
                }
            }
            else
            {
                req.ContentType = "application/x-www-form-urlencoded";
                StringBuilder sb = new StringBuilder();
                foreach (string key in postingData.Keys)
                {
                    sb.AppendFormat("{0}={1}&", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(postingData[key]));
                }
                if (sb.Length > 0)
                    sb.Length--;
                writer.Write(sb.ToString());
            }

            writer.Flush();

            using (Stream stream = req.GetRequestStream())
            {
                memoryStream.WriteTo(stream);
            }
        }

        if (startPoint != 0 && endPoint != 0)
            req.AddRange(startPoint, endPoint);
        else if (startPoint != 0 && endPoint == 0)
            req.AddRange(startPoint);

        return req;
    }

    /// <summary>
    /// 發出一次新的請求,并返回獲得的回應
    /// 調用此方法永遠不會觸發StatusUpdate事件.
    /// </summary>
    /// <returns>相應的HttpWebResponse</returns>
    public HttpWebResponse GetResponse()
    {
        HttpWebRequest req = CreateRequest();
        HttpWebResponse res = (HttpWebResponse)req.GetResponse();
        responseHeaders = res.Headers;
        if (keepContext)
        {
            context.Cookies = res.Cookies;
            context.Referer = url;
        }
        return res;
    }

    /// <summary>
    /// 發出一次新的請求,并返回回應內容的流
    /// 調用此方法永遠不會觸發StatusUpdate事件.
    /// </summary>
    /// <returns>包含回應主體內容的流</returns>
    public Stream GetStream()
    {
        return GetResponse().GetResponseStream();
    }

    /// <summary>
    /// 發出一次新的請求,并以字節數組形式返回回應的內容
    /// 調用此方法會觸發StatusUpdate事件
    /// </summary>
    /// <returns>包含回應主體內容的字節數組</returns>
    public byte[] GetBytes()
    {
        HttpWebResponse res = GetResponse();
        int length = (int)res.ContentLength;

        MemoryStream memoryStream = new MemoryStream();
        byte[] buffer = new byte[0x100];
        Stream rs = res.GetResponseStream();
        for (int i = rs.Read(buffer, 0, buffer.Length); i > 0; i = rs.Read(buffer, 0, buffer.Length))
        {
            memoryStream.Write(buffer, 0, i);
            OnStatusUpdate(new StatusUpdateEventArgs((int)memoryStream.Length, length));
        }
        rs.Close();

        return memoryStream.ToArray();
    }

    /// <summary>
    /// 發出一次新的請求,以Http頭,或Html Meta標簽,或DefaultEncoding指示的編碼信息對回應主體解碼
    /// 調用此方法會觸發StatusUpdate事件
    /// </summary>
    /// <returns>解碼后的字符串</returns>
    public string GetString()
    {
        byte[] data = GetBytes();
        string encodingName = GetEncodingFromHeaders();

        if (encodingName == null)
            encodingName = GetEncodingFromBody(data);

        Encoding encoding;
        if (encodingName == null)
            encoding = defaultEncoding;
        else
        {
            try
            {
                encoding = Encoding.GetEncoding(encodingName);
            }
            catch (ArgumentException)
            {
                encoding = defaultEncoding;
            }
        }
        return encoding.GetString(data);
    }

    /// <summary>
    /// 發出一次新的請求,對回應的主體內容以指定的編碼進行解碼
    /// 調用此方法會觸發StatusUpdate事件
    /// </summary>
    /// <param name="encoding">指定的編碼</param>
    /// <returns>解碼后的字符串</returns>
    public string GetString(Encoding encoding)
    {
        byte[] data = GetBytes();
        return encoding.GetString(data);
    }

    private string GetEncodingFromHeaders()
    {
        string encoding = null;
        string contentType = responseHeaders["Content-Type"];
        if (contentType != null)
        {
            int i = contentType.IndexOf("charset=");
            if (i != -1)
            {
                encoding = contentType.Substring(i + 8);
            }
        }
        return encoding;
    }

    private string GetEncodingFromBody(byte[] data)
    {
        string encodingName = null;
        string dataAsAscii = Encoding.ASCII.GetString(data);
        if (dataAsAscii != null)
        {
            int i = dataAsAscii.IndexOf("charset=");
            if (i != -1)
            {
                int j = dataAsAscii.IndexOf("\"", i);
                if (j != -1)
                {
                    int k = i + 8;
                    encodingName = dataAsAscii.Substring(k, (j - k) + 1);
                    char[] chArray = new char[2] { '>', '"' };
                    encodingName = encodingName.TrimEnd(chArray);
                }
            }
        }
        return encodingName;
    }

    /// <summary>
    /// 發出一次新的Head請求,獲取資源的長度
    /// 此請求會忽略PostingData, Files, StartPoint, EndPoint, Verb
    /// </summary>
    /// <returns>返回的資源長度</returns>
    public int HeadContentLength()
    {
        Reset();
        HttpVerb lastVerb = verb;
        verb = HttpVerb.HEAD;
        using (HttpWebResponse res = GetResponse())
        {
            verb = lastVerb;
            return (int)res.ContentLength;
        }
    }

    /// <summary>
    /// 發出一次新的請求,把回應的主體內容保存到文件
    /// 調用此方法會觸發StatusUpdate事件
    /// 如果指定的文件存在,它會被覆蓋
    /// </summary>
    /// <param name="fileName">要保存的文件路徑</param>
    public void SaveAsFile(string fileName)
    {
        SaveAsFile(fileName, FileExistsAction.Overwrite);
    }

    /// <summary>
    /// 發出一次新的請求,把回應的主體內容保存到文件
    /// 調用此方法會觸發StatusUpdate事件
    /// </summary>
    /// <param name="fileName">要保存的文件路徑</param>
    /// <param name="existsAction">指定的文件存在時的選項</param>
    /// <returns>是否向目標文件寫入了數據</returns>
    public bool SaveAsFile(string fileName, FileExistsAction existsAction)
    {
        byte[] data = GetBytes();
        switch (existsAction)
        {
            case FileExistsAction.Overwrite:
                using (BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)))
                    writer.Write(data);
                return true;

            case FileExistsAction.Append:
                using (BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.Append, FileAccess.Write)))
                    writer.Write(data);
                return true;

            default:
                if (!File.Exists(fileName))
                {
                    using (
                        BinaryWriter writer =
                            new BinaryWriter(new FileStream(fileName, FileMode.Create, FileAccess.Write)))
                        writer.Write(data);
                    return true;
                }
                else
                {
                    return false;
                }
        }
    }
}

public class HttpClientContext
{
    private CookieCollection cookies;
    private string referer;

    public CookieCollection Cookies
    {
        get { return cookies; }
        set { cookies = value; }
    }

    public string Referer
    {
        get { return referer; }
        set { referer = value; }
    }
}

public enum HttpVerb
{
    GET,
    POST,
    HEAD,
}

public enum FileExistsAction
{
    Overwrite,
    Append,
    Cancel,
}

public class HttpUploadingFile
{
    private string fileName;
    private string fieldName;
    private byte[] data;

    public string FileName
    {
        get { return fileName; }
        set { fileName = value; }
    }

    public string FieldName
    {
        get { return fieldName; }
        set { fieldName = value; }
    }

    public byte[] Data
    {
        get { return data; }
        set { data = value; }
    }

    public HttpUploadingFile(string fileName, string fieldName)
    {
        this.fileName = fileName;
        this.fieldName = fieldName;
        using (FileStream stream = new FileStream(fileName, FileMode.Open))
        {
            byte[] inBytes = new byte[stream.Length];
            stream.Read(inBytes, 0, inBytes.Length);
            data = inBytes;
        }
    }

    public HttpUploadingFile(byte[] data, string fileName, string fieldName)
    {
        this.data = data;
        this.fileName = fileName;
        this.fieldName = fieldName;
    }
}

public class StatusUpdateEventArgs : EventArgs
{
    private readonly int bytesGot;
    private readonly int bytesTotal;

    public StatusUpdateEventArgs(int got, int total)
    {
        bytesGot = got;
        bytesTotal = total;
    }

    /// <summary>
    /// 已經下載的字節數
    /// </summary>
    public int BytesGot
    {
        get { return bytesGot; }
    }

    /// <summary>
    /// 資源的總字節數
    /// </summary>
    public int BytesTotal
    {
        get { return bytesTotal; }
    }
}

}</pre>

.Net類庫里提供了HttpWebRequest等類,方便我們編程與Web服務器進行交互. 但是實際使用中我們經常會遇到以下需求, 基礎類里沒有直接提供相應的功能 (WebClient類包含這些功能,只是用起來稍微麻煩一點--謝謝網友東吳居士的提醒):

  • 對HttpWebResponse獲取的HTML進行文字編碼轉換,使之不會出現亂碼;
  • 自動在Session間保持Cookie,Referer等相關信息;
  • 模擬HTML表單提交;
  • 向服務器上傳文件;
  • 對二進制的資源,直接獲取返回的字節數組(byte[]),或者保存為文件

為了解決這些問題,我開發了HttpClient類.下面是使用的方法:

  • 獲取編碼轉換后的字符串

    HttpClient client=new HttpClient(url);
    string html=client.GetString();

    GetString()函數內部會查找Http Headers, 以及HTML的Meta標簽,試圖找出獲取的內容的編碼信息.如果都找不到,它會使用client.DefaultEncoding, 這個屬性默認為utf-8, 也可以手動設置.
  • 自動保持Cookie, Referer

    HttpClient client=new HttpClient(url1, null, true);
    string html1=client.GetString();
    client.Url=url2;
    string html2=client.GetString();

    這里HttpClient的第三個參數,keepContext設置為真時,HttpClient會自動記錄每次交互時服務器對Cookies進行的操作,同時會以前一次請求的Url為Referer.在這個例子里,獲取html2時,會把url1作為Referer, 同時會向服務器傳遞在獲取html1時服務器設置的Cookies. 當然,你也可以在構造HttpClient時直接提供第一次請求要發出的Cookies與Referer:

    HttpClient client=new HttpClient(url, new WebContext(cookies, referer), true);

    或者,在使用過程中隨時修改這些信息:

    client.Context.Cookies=cookies;
    client.Context.referer=referer;
  • 模擬HTML表單提交

    HttpClient client=new HttpClient(url);
    client.PostingData.Add(fieldName1, filedValue1);
    client.PostingData.Add(fieldName2, fieldValue2);
    string html=client.GetString();

    上面的代碼相當于提交了一個有兩個input的表單. 在PostingData非空,或者附加了要上傳的文件時(請看下面的上傳和文件), HttpClient會自動把HttpVerb改成POST, 并將相應的信息附加到Request上.
  • 向服務器上傳文件

    HttpClient client=new HttpClient(url);
    client.AttachFile(fileName, fieldName);
    client.AttachFile(byteArray, fileName, fieldName);
    string html=client.GetString();

    這里面的fieldName相當于<input type="file" name="fieldName" />里的fieldName. fileName當然就是你想要上傳的文件路徑了. 你也可以直接提供一個byte[] 作為文件內容, 但即使如此,你也必須提供一個文件名,以滿足HTTP規范的要求.
  • 不同的返回形式

    字符串: string html = client.GetString();
    流: Stream stream = client.GetStream();
    字節數組: byte[] data = client.GetBytes();
    保存到文件:  client.SaveAsFile(fileName);
    或者,你也可以直接操作HttpWebResponse: HttpWebResponse res = client.GetResponse();

    每調用一次上述任何一個方法,都會導致發出一個HTTP Request, 也就是說,你不能同時得到某個Response的兩種返回形式.
    另外,調用后它們任意一個之后,你可以通過client.ResponseHeaders來獲取服務器返回的HTTP頭.
  • 下載資源的指定部分(用于斷點續傳,多線程下載)

    HttpClient client=new HttpClient(url);
    //發出HEAD請求,獲取資源長度
    int length=client.HeadContentLength();

    //只獲取后一半內容
    client.StartPoint=length/2;
    byte[] data=client.GetBytes();

    HeadContentLength()只會發出HTTP HEAD請求.根據HTTP協議, HEAD與GET的作用等同, 但是,只返回HTTP頭,而不返回資源主體內容. 也就是說,用這個方法,你沒法獲取一個需要通過POST才能得到的資源的長度,如果你確實有這樣的需求,建議你可以通過GetResponse(),然后從ResponseHeader里獲取Content-Length.

計劃中還有另外一些功能要加進來,比如斷點續傳, 多線程下載, 下載進度更新的事件機制等, 正在思考如何與現在的代碼融合到一起,期待你的反饋.

注意:使用時應該添加對System.Web.dll的引用,并在使用此類的代碼前添加"using System.Web;",不然會無法通過編譯.

 本文由用戶 openkk 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!