天天看點

關于HTTP請求的那些事兒

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

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

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

看格式其實也沒多複雜,我們下面會着重說的是請求行裡的

請求方法

部分請求頭

請求行

請求行裡包括:請求方法、URL和協定版本。

常見的請求方法有

GET

POST

PUT

DELETE

PATCH

,當然 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

4. Cookie

參考文獻:

RFC2616

MDN Doc MIME_types

https://www.runoob.com/http/http-content-type.html

https://learning.postman.com/docs/sending-requests/requests/