1. 程式人生 > 實用技巧 >關於HTTP請求的那些事兒

關於HTTP請求的那些事兒

關於 HTTP 請求,如果你知道有GET、POST請求,GET是在url 裡用鍵值對傳參,POST 只是換一個請求方法或者有時還可以傳送一些json格式引數的話。如果你還想知道為什麼有時候用 Ajax 請求明明和介面要求的引數一致卻提示4xx之類的錯誤,為什麼客戶端請求的引數和伺服器收到的不一致,為什麼伺服器端收不到上傳的檔案這類問題的話,那就請繼續往下看吧。

有些基礎知識你可能之前就知道

HTTP 請求是由請求行(request line)、請求頭部(header)、空行和請求資料四部分組成的,一般格式是這樣的:

看格式其實也沒多複雜,我們下面會著重說的是請求行裡的 請求方法部分請求頭

請求行

請求行裡包括:請求方法、URL和協議版本。
常見的請求方法有 GETPOSTPUTDELETEPATCH,當然 HTTP 協議裡還定義了更多的請求方法,如果你使用過 Postman (下面的示例會用此工具做演示)位址列左側就提供請求方法的選擇框如圖:

URL 可以包含請求地址和引數,類似https://www.baidu.com?k=xxx&from=xxx
協議版本就是HTTP的版本號,到現在為止總共經歷了3個版本的演化,從0.9 -> 1.0 -> 1.1 -> 2.0,但常用的還是 HTTP/1.1

要傳送一個簡單的請求其實只要有 URL 和 HTTP Method就夠了,比如我們在瀏覽器裡輸入https://www.baidu.com/

,瀏覽器會預設使用 GET 方法、預設的協議版本和請求頭髮起請求,你就可以看到百度首頁了。

請求資料 BODY

請求的本質其實是完成客戶端和伺服器端的資料互動,無論通過什麼方式的請求,客戶端把需要告訴伺服器端資訊以引數的形式傳遞過去就完成了請求。簡單且常用的就是 GET 請求沒有請求 body ,所以請求引數是在 URL 裡跟在問號(?)後邊的,以等號(=)分隔的鍵值對,如:?id=1&type=new這種形式的,POST 請求可以把更復雜的資訊傳給伺服器端,就需要把資訊放在 body 裡,但是 body 裡的資料是什麼格式的還需要額外的告訴伺服器端,也就是請求頭裡各個欄位的作用。

請求頭

RFC2616 中定義了47種請求頭欄位,每個欄位都有對應的作用,這裡是對特殊的幾個做說明。

1. Content 相關

請求內容相關的欄位有:Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range和Content-Type。這裡主要說Content-Type顧名思義就是body裡內容的型別,文章開頭說的幾個問題,多數情況是因為對這個欄位理解不足導致的。
Content-Type 用於指示資源的MIME型別 media type 。常見的Content-Type格式是:Content-Type: text/html; charset=utf-8,分號前面是媒體型別也就是型別的可選值,後邊是字元編碼。還有一種不常用的瞭解一下就可以了,格式是這樣的Content-Type: multipart/form-data; boundary=something,這類請求包含多部分請求體,boundary 是一組由1到70個字元組成的分隔符,用來分隔請求體。

請求中 (如POST 或 PUT),Content-Type客戶端通過告訴伺服器實際傳送的資料型別。

  • application/json : JSON資料格式,在微信小程式和vue的一些請求庫中的content-type一般預設為該型別。
  • application/x-www-form-urlencoded : HTML中 form 的 encType預設值,表單的資料將被編碼為key/value格式傳送到伺服器。
  • multipart/form-data : 需要在表單中進行檔案上傳時需要使用該格式。
    下面用C#寫了一個工具類,用來模擬請求:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
namespace My.Helper
{
    public class WebUtils
    {
        private static int _timeout = 60000;
        public static HttpWebRequest GetWebRequest(string url, string method, CookieContainer cookies = null)
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
            req.ServicePoint.Expect100Continue = false;
            req.Method = method;
            req.KeepAlive = true;
            req.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)";
            req.Timeout = _timeout;
            if (cookies != null)
            {
                req.CookieContainer = cookies;
            }

            return req;
        }

        /// <summary>
        /// 執行 GET 請求。
        /// </summary>
        /// <param name="url">請求地址</param>
        /// <param name="parameters">請求引數</param>
        /// <returns>HTTP響應</returns>
        public static string DoGet(string url, IDictionary<string, string> parameters)
        {
            url = BuildGetUrl(url, parameters);

            HttpWebRequest req = GetWebRequest(url, "GET");
            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }

        /// <summary>
        /// 執行 POST 請求
        /// </summary>
        /// <param name="url"></param>
        /// <param name="jsonObject"></param>
        /// <returns></returns>
        public static string DoPost(string url, object jsonObject)
        {
            HttpWebRequest req = GetWebRequest(url, "POST");
            req.ContentType = "application/json;charset=utf-8";

            byte[] postData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jsonObject));
            Stream reqStream = req.GetRequestStream();
            reqStream.Write(postData, 0, postData.Length);
            reqStream.Close();

            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }



        /// <summary>
        /// 執行模擬Form提交的 POST 請求 
        /// </summary>
        /// <param name="url"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static string DoPost(string url, IDictionary<string, string> parameters)
        {
            HttpWebRequest req = GetWebRequest(url, "POST");
            req.ContentType = "application/x-www-form-urlencoded;charset=utf-8";

            byte[] postData = Encoding.UTF8.GetBytes(BuildQuery(parameters));
            Stream reqStream = req.GetRequestStream();
            reqStream.Write(postData, 0, postData.Length);
            reqStream.Close();

            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }

        /// <summary>
        /// 執行帶檔案上傳的HTTP POST請求。
        /// </summary>
        /// <param name="url">請求地址</param>
        /// <param name="textParams">請求文字引數</param>
        /// <param name="fileParams">請求檔案引數</param>
        /// <returns>HTTP響應</returns>
        public static string DoPost(string url, IDictionary<string, string> textParams, IDictionary<string, FileItem> fileParams)
        {
            // 如果沒有檔案引數,則走普通POST請求
            if (fileParams == null || fileParams.Count == 0)
            {
                return DoPost(url, textParams);
            }

            string boundary = DateTime.Now.Ticks.ToString("X"); // 隨機分隔線

            HttpWebRequest req = GetWebRequest(url, "POST");
            req.ContentType = "multipart/form-data;charset=utf-8;boundary=" + boundary;

            Stream reqStream = req.GetRequestStream();
            byte[] itemBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n");
            byte[] endBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");

            // 組裝文字請求引數
            string textTemplate = "Content-Disposition:form-data;name=\"{0}\"\r\nContent-Type:text/plain\r\n\r\n{1}";
            IEnumerator<KeyValuePair<string, string>> textEnum = textParams.GetEnumerator();
            while (textEnum.MoveNext())
            {
                string textEntry = string.Format(textTemplate, textEnum.Current.Key, textEnum.Current.Value);
                byte[] itemBytes = Encoding.UTF8.GetBytes(textEntry);
                reqStream.Write(itemBoundaryBytes, 0, itemBoundaryBytes.Length);
                reqStream.Write(itemBytes, 0, itemBytes.Length);
            }

            // 組裝檔案請求引數
            string fileTemplate = "Content-Disposition:form-data;name=\"{0}\";filename=\"{1}\"\r\nContent-Type:{2}\r\n\r\n";
            IEnumerator<KeyValuePair<string, FileItem>> fileEnum = fileParams.GetEnumerator();
            while (fileEnum.MoveNext())
            {
                string key = fileEnum.Current.Key;
                FileItem fileItem = fileEnum.Current.Value;
                string fileEntry = string.Format(fileTemplate, key, fileItem.FileName, fileItem.MimeType);
                byte[] itemBytes = Encoding.UTF8.GetBytes(fileEntry);
                reqStream.Write(itemBoundaryBytes, 0, itemBoundaryBytes.Length);
                reqStream.Write(itemBytes, 0, itemBytes.Length);

                byte[] fileBytes = fileItem.Content;
                reqStream.Write(fileBytes, 0, fileBytes.Length);
            }

            reqStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
            reqStream.Close();

            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }

        /// <summary>
        /// 組裝GET請求URL。
        /// </summary>
        /// <param name="url">請求地址</param>
        /// <param name="parameters">請求引數</param>
        /// <returns>帶引數的GET請求URL</returns>
        private static string BuildGetUrl(string url, IDictionary<string, string> parameters)
        {
            if (parameters != null && parameters.Count > 0)
            {
                if (url.Contains("?"))
                {
                    url = url + "&" + BuildQuery(parameters);
                }
                else
                {
                    url = url + "?" + BuildQuery(parameters);
                }
            }
            return url;
        }

        /// <summary>
        /// 組裝普通文字請求引數。
        /// </summary>
        /// <param name="parameters">Key-Value形式請求引數字典</param>
        /// <returns>URL編碼後的請求資料</returns>
        private static string BuildQuery(IDictionary<string, string> parameters)
        {
            StringBuilder postData = new StringBuilder();
            bool hasParam = false;

            IEnumerator<KeyValuePair<string, string>> dem = parameters.GetEnumerator();
            while (dem.MoveNext())
            {
                string name = dem.Current.Key;
                string value = dem.Current.Value;
                // 忽略引數名或引數值為空的引數
                if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(value))
                {
                    if (hasParam)
                    {
                        postData.Append("&");
                    }
                    postData.Append(name);
                    postData.Append("=");
                    postData.Append(Uri.EscapeDataString(value));
                    //postData.Append(value);
                    hasParam = true;
                }
            }

            return postData.ToString();
        }

        private static byte[] GetResponseAsBytes(HttpWebResponse rsp)
        {
            using (var input = rsp.GetResponseStream())
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    input.CopyTo(ms);
                    return ms.ToArray();
                }
            }
        }

        private static string GetResponseAsString(HttpWebResponse rsp, Encoding encoding)
        {
            using (var stream = rsp.GetResponseStream())
            {
                using (var reader = new StreamReader(stream, encoding))
                {
                    return reader.ReadToEnd();
                }
            }
        }
    }
}

響應中,Content-Type標頭告訴客戶端實際返回的內容的內容型別。瀏覽器會在某些情況下進行MIME查詢,並不一定遵循此標題的值; 為了防止這種行為,可以將標題 X-Content-Type-Options 設定為 nosniff。這裡最直觀的就是在IIS裡有個MIME型別設定,並且設定一些預設的配置。


但是當你網站提供的內容型別不在預設配置中的時候,web站點就不知道以什麼型別返回給客戶端或者是直接不響應了。這就是為什麼有時候明明檔案放在了伺服器客戶卻請求不到,例如:安卓或蘋果的安裝檔案.apk/.ipa ,一些音視訊檔案.mp3.mp4播放不了,一些字型檔案.woff找不到等,這類情況只需在IIS配置新增相應的型別就行或者在web.config檔案裡新增下面的節點:

<staticContent>
   <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
   <mimeMap fileExtension=".mp4" mimeType="video/mp4" />
   <mimeMap fileExtension=".apk" mimeType="application/vnd.android" />
</staticContent>

還有一種情況是伺服器端錯誤返回content-type,當伺服器不能正確識別PDF檔案時,會把本該返回的application/pdf設定為了application/octet-stream,瀏覽器拿到的是octet-stream只能當檔案下載,不能直接開啟。
詳細的MIME型別和檔案字尾的對應可以檢視這裡媒體型別(MIME types)

查資料時發現這麼一個話題,感興趣的可以看看這個討論是否要需要在GET 請求中指定 Content-Type

斷斷續續寫了2天,感覺要寫的東西太多了,後邊還有幾個話題只列了標題,有時間了再補吧,先發上來跟大家共同學習討論吧...

2. 快取 Cache-Control

3. 授權 Authorization

參考文獻:
RFC2616
MDN Doc MIME_types
https://www.runoob.com/http/http-content-type.html
https://learning.postman.com/docs/sending-requests/requests/