我知道的HTTP請求

HTTP你們都不陌生,可是HTTP的許多細節就並非不少人都知道了,本文將討論一些容易被忽略但又比較重要的點。css

首先,怎麼用原生JS寫一個GET請求呢?以下代碼,只需3行:html

let xhr = new XMLHttpRequest();
xhr.open("GET", "/list");
xhr.send();
複製代碼

xhr.open第一個參數是請求方法,第二個參數是請求url,而後把它send出去就好了。webpack

若是須要加上請求參數,若是用jQuery的ajax,那麼是這麼寫的:nginx

$.ajax({
    url: "/list",
    data: {
        page: 5
    }
});複製代碼

若是是用原生的話就得拼在請求url上面,即open的第二個參數:git

而且參數須要轉義,以下代碼所示:github

function ajax (url, data) {
    let args = [];
    for (let key in data) {
        // 參數須要轉義
      args.push(`${encodeURIComponent(key)} = 
                                     ${encodeURIComponent(data[key])}`);
    }
    let search = args.join("&");
    // 判斷當前url是否已有參數
    url += ~url.indexOf("?") ? `&${search}` : `?${search}`;

    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
}複製代碼

那爲何用jq就不用呢?由於jq幫咱們作了,jq的ajax支持一個叫processData的參數,默認爲true:web

$.ajax({
    url: "/list",
    data: {
        page: 5
    },
    processData: true
});
複製代碼

這個參數的做用是用來處理傳進來的data的,以下jq的源碼:ajax

若是傳了data,而且processData爲true,而且data不是一個string了,就會調param處理data。而後咱們來看下這個param函數是怎麼實現的:數據庫

能夠看到,它也是跟我本身實現的ajax相似,把key和value轉義用"="拼接,而後push到一個數組,最後再join地一下。不同的地方是它的判斷邏輯比個人複雜,它會再調一個buildParams的函數去處理key/value,由於value多是一個數組,也多是一個Object。若是value是一個Object,那麼直接encode一下就會變成"[object Object]"的轉義了:json

因此buildParams在處理每一個key/value時,會先判斷當前value是不是一個數組或者是Object,以下圖所示:

若是它是一個數組的話,這個數組的每個元素都會變成單獨的一個請求字段,它的key是父級的key拼上數組的索引獲得,如{ids: [1, 2, 3]}就會被拼成:ids[0]=一、ids[1] = 二、ids[2] = 3,若是是一個Object的話key的後綴就是子Object的key,如{user: {id: 1333, name: "yin"}}會被拼成:user[id]=133三、user[name]=yin,不然就認爲它是一個簡單的類型,就直接調一下param函數定義的add,把它push到s那個數組。這裏用到了遞歸調用,會不斷地拼key值,直到value是一個普通變量了,就到了最後面的else邏輯。

也就是說,如下代碼:

$.ajax({
    url: "/list",
    data: {
        user: {
            name: "yin",
            age: 18
        }
    },
});複製代碼

將會被拼成的url爲:

/list?user[name]=yin&user[age]=18

注意上面的中括號尚未轉義。而若是是一個數組的話:

$.ajax({
    url: "/list",
    data: {
        ids: [1, 2, 3]
    },
});複製代碼

拼成的請求url爲:

/list?ids[0]=1&ids[1]=2&ids[2]=3

若是後端用的Java的Spring MVC框架的話,是理解這種格式的,框架收到這樣的參數後會生成一個Array,傳遞給業務代碼,業務代碼是不用關心怎麼處理這種參數的。其它的框架應該也相似。


怎麼用原生JS寫一個POST請求呢?以下圖所示:

POST請求的參數就不是放在url了,而是放在send裏面,即請求體。你可能會問:難道就不能放url麼?我就要放url。若是你夠任性,那麼能夠,前提是後端所使用的http框架可以在url裏面取數據,由於它必定會收到url,也必定會收到請求體,因此取決於它要怎麼處理,按照http標準,若是請求方法是POST,那麼應該是得去請求體拿的,就不會在url的search上取了,固然它能夠改一下,改爲兩個均可以拿。

而後咱們會發現請求的mime類型是text/plain:

而且查看請求參數的時候,並非平時所看到可以按照字段一行行地展現:

這是爲何呢?這是由於咱們沒有設置它的Content-Type,以下代碼:

let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.setRequestHeader("Content-type", 
                           "application/x-www-form-urlencoded");
xhr.send("id=5&name=yin");複製代碼

若是設置Content-Type爲x-www-form-urlencoded的話,那麼在檢查的話,Chrome也會按字段分行展現了:

這個也是jq默認的Content-Type:

它是一種最經常使用的一種請求編碼方式,支持GET/POST等方法,特色是:全部的數據變成鍵值對的形式key1=value1&key2=value2的形式,而且特殊字符須要轉義成utf-8編號,如空格會變成%20:

因爲中文在utf-8須要佔用3個字節,因此它有3個%符號。


咱們剛剛是xhr.send一個字符串,若是send一個Object會怎麼樣呢?以下代碼所示:

let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.send({id:5, name: "yin"});複製代碼

檢查控制檯的時候是這樣的:

也就是說,其實是調了Object的toString方法。因此能夠看到,在send的數據須要轉成字符串

除了字符串以外,send還支持FormData/Blob等格式,如:

let form = $("form")[0];
xhr.send(new FormData(form));
複製代碼

但最後都是被轉成字符串發送。


咱們再看下其它的請求格式,如Github的REST API是使用json的格式發請求:

這個時候要求格式要變成json,就須要指定Content-Type爲application/json,而後send的數據要stringify一下:

let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.setRequestHeader("Content-type", "application/json");
let data = {id:5, name: "yin"};
xhr.send(JSON.stringify(data));複製代碼

若是是用jq的話,那麼能夠這樣:

$.ajax({
    processData: false,
    data: JSON.stringify(data),
    contentType: "application/json"
});
複製代碼

這個時候processData爲false,告訴jq不要處理數據了——即拼成key1=value1&key2=value2的形式,直接把傳給它的數據send就行了。

咱們能夠比較json和urlencoded這兩種形式的優缺點,json的缺點是parse解析的工做量要明顯高於split("&")的工做量,可是json的優勢又在於表達複雜結構的時候比較簡潔,如二維數組(m * n)在urlencoded須要拆成m * n個字段,而json就不用了。因此相對來講,若是請求數據結構比較簡單應該是使用經常使用的urlencoded比較有利,而比較複雜時使用json比較有利。一般來講比較經常使用的仍是urlencoded.


還有第3種常見的編碼是multipart/form-data,這種也能夠用來發請求,以下代碼所示:

let formData = new FormData();
formData.append("id", 5); // 數字5會被當即轉換成字符串 "5"
formData.append("name", "#yin");
// formData.append("file", input.files[0]);
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.send(formData);複製代碼

它一般用於上傳文件,上傳文件必需要使用這種格式。上面代碼發送的內容以下圖所示:

每一個字段之間用一個隨機字符串隔開,保證發送的內容不會出現這個字符串,這樣發送的內容就不須要進行轉義了,由於若是文件很大的話,轉義須要花費至關的時間,體積也可能會成倍地增加。


而後再討論一個問題,咱們知道在瀏覽器地址欄輸入一個網址請求數據,這個時候是用的GET請求,而咱們在代碼裏面用ajax發的GET請求也是GET,瀏覽器訪問網址的GET和ajax的GET有什麼區別嗎

爲了可以觀察到瀏覽器自已發出去的GET,須要用一個抓包工具看一下這個GET是怎麼樣的,以下圖所示:

瀏覽器本身發的GET有一個明顯的特色,它會設置http請求頭的Accept字段,而且把text/html排在第一位,即它最但願收到的是html格式。而動態的ajax抓包顯示是這樣的:

能夠看到,使用地址欄訪問的和使用ajax的get請求本質上都是同樣的,只是使用ajax咱們能夠設置http請求頭,而使用地址欄訪問的是由瀏覽器添加默認的請求頭。


上面是使用http抓的包,咱們能夠看到請求的完整url,包括請求的參數,而若是是抓的https的包的話,GET放在url上的參數就看不到了:

也就是說https的請求報文是加密的,包括請求的uri等,須要解密後才能看到。那是否是說使用https的GET也是安全的,而不是https的POST不會比GET安全?

咱們先來看一下http的請求報文,以下圖所示:

若是使用抓包工具的話,能夠看到請求報文確實是按照上圖排列的,如圖所示的GET:

而POST是這樣的:

因此本質上GET/POST是同樣的,只是GET把數據拼到url,而POST是放到請求體。另一點,url是有長度限制的,包括瀏覽器和接收的服務如nginx,而請求體是沒有限制的(瀏覽器沒有限制,可是通常nginx接收會有限制),還有POST的數據支持多種編碼格式。

雖然如此,POST仍是比GET安全的,體如今如下幾點:

  1. GET參數是放在url上面,用戶能夠保存爲書籤、傳播連接,若是參數有敏感數據,如登錄的密碼,那麼可能會泄露
  2. 搜索引擎在爬取網站的時候若是修改數據庫的請求支持GET,那麼極可能數據庫會無心被搜索引擎修改
  3. script/img等標籤是GET請求,會更加方便跨站請求僞造,在瀏覽器地址欄輸入也是GET,也是爲修改請求提供便利


接着討論請求響應狀態碼。不少人都知道200、40四、500這幾個,對於其它的可能就不甚瞭解了。這裏我把一些經常使用的狀態碼列一下。

1. 301 永久轉移

當你想換域名的時候,就可使用301,如以前的域名叫www.renfed.com,後來換了一個新域名www.rrfed.com,但願用戶訪問老域名的時候可以自動跳轉到新的域名,那麼就可使用nginx返回301:

server {
    listen       80;
    server_name  www.rrfed.com;
    root         /home/fed/wordpress;
    return       301 https://www.rrfed.com$request_uri;
}複製代碼

瀏覽器收到301以後,就會自動跳轉了。搜索引擎在爬的時候若是發現是301,在若干天以後它會把以前收錄的網頁的域名給換了。

還有一個場景,若是但願訪問http的時候自動跳轉到https也是能夠用301,由於若是直接在瀏覽器地址欄輸入域名而後按回車,前面沒有帶https,那麼是默認的http協議,這個時候咱們但願用戶可以訪問安全的https的,不要訪問http的,因此要作一個重定向,也可使用301,如:

server {
    listen       80; 
    server_name  www.rrfed.com;

    if ($scheme != "https") {
         return 301 https://$host$request_uri;
    }   
}
複製代碼

2. 302 Found 資源暫時轉移

不少短連接跳轉長連接就是使用的302,以下圖所示:

3. 304 Not Modified 沒有修改

在本地使用webpack-dev-server開發的時候,若是沒有改js/css,那麼刷新頁面的時候請求本地的js/css文件將返回304,以下圖所示:

webpack-dev-server的服務怎麼知道沒有修改呢,由於瀏覽器在請求的時候帶上了etag,如:

W/"10e632-Oz38I6asQyS459XpsaJYkjMUoZI"

服務會計算當前文件的etag,它是一個文件的哈希值,而後比較一下傳過來的etag,若是相等,則認爲沒有修改返回304。若是有修改的話就會返回200和文件的內容,並從新給瀏覽器一個新的etag。下次請求的時候瀏覽器就會帶上這個新的etag。若是把控制檯的disable cached打開的話,那麼瀏覽器即便有etag也不會帶上。另一個判斷有沒有修改的字段是Last Modified Time,這是根據文件的修改時間。

4. 400 Bad Request 請求無效

當必要參數缺失、參數格式不對時,後端一般會返回400,以下圖所示:

並帶上了提示信息:

{"message":"opportunityId type mismatch required type 'long' "}

經過400能夠知道請求參數有誤,結合提示信息,說明須要傳一個數字,而不是字符串。

5. 403 Forbidden 拒絕服務

服務可以理解你的請求,包括傳參正確,可是拒絕提供服務。例如,服務容許直接訪問靜態文件:

可是不容許訪問某個目錄:

不然,別人對你服務器上的文件就一覽無遺了。

403和401的區別在於,401是沒有認證,沒有登錄驗證之類的錯誤。

6. 500 內部服務器錯誤

如業務代碼出現了異常沒有捕獲,被tomcat捕獲了,就會返回500錯誤:

如:數據庫字段長度限制爲30個字符,若是沒有判斷直接插入一條31個字符的記錄,就會致使數據庫拋異常,若是異常沒有捕獲處理,就直接返回500。

當服務完全掛了,連返回都沒有的時候,那麼就是502了。

7. 502 Bad Gateway 網關錯誤

以下圖所示:

這種狀況是由於nginx收到請求,可是請求沒有打過去,多是由於業務服務掛了,或者是打過去的端口號寫錯了:

server {
    location / {
        # webpack的服務
        proxy_pass https://127.0.0.1:7071;
    }
}
複製代碼

nginx返回了502.

8. 504 Gateway Timeout 網關超時

一般是由於服務處理請求過久,致使超時,如PHP服務默認的請求響應最長處理時間爲30s,若是超過30s,將會掛掉,返回504,以下圖所示:

這種狀況多是由於服務還要請求第三方的服務,第三方服務處理時間較久沒有返回,如在向FCM發送Push的時候,若是一個請求裏面須要發送的訂閱的瀏覽器(subscriptions)太多了,就常常會處理好久,致使504.

9. 101 協議轉換

websocket是從http升級而來,在創建鏈接前須要先經過http進行協議升級:

還有一個600,600是一種不太經常使用的狀態碼,表示服務器沒有返回響應頭部,只返回實體內容。

這些狀態碼實際上就是一個數字,能夠任意返回,可是最好是按照http的規定返回合適的狀態碼。若是返回四、五、6開頭的http狀態碼,瀏覽器將會打印錯誤,認爲當前請求失敗。


咱們尚未說怎麼判斷請求成功了,以下代碼所示:

xhr.open("POST", UPLOAD_URL);
xhr.onreadystatechange = function() {
    // readyState爲4表示請求完成
    if (this.readyState === 4){
        if (this.status === 200) {
            let response = JSON.parse(this.responseText);
            if (!response.status || response.status.code !== 0) {
                // 失敗     
                callback.failed && callback.failed();
            } else {    
                // 成功     
                callback.success(response.data.url);
            }           
        } else if (this.status >= 400 || this.status === 0) {
            // 失敗     
            callback.failed && callback.failed();
        // 正常不該該返回20幾的狀態碼,這種狀況也認爲是失敗
        } else {    
            callback.failed && callback.failed();
        }
    }
};
xhr.send(formData);複製代碼

這裏有個問題,若是返回的狀態碼是3開頭的重定向,須要本身再去發一個請求嗎?

實踐證實,不須要,瀏覽器會自動重定向,以下圖所示:


最後,本文提到了3種經常使用的請求編碼,分別是application/www-x-form-urlencoded、application/json、multipart/form-data,第一種是最經常使用的一種,適用於GET/POST等,第二種常見於請求響應的數據格式,第三種一般用於上傳文件。而後還比較了POST和GET,雖然二者的請求數據都在http報文裏面,只是位置不同,可是考慮到用戶、搜索引擎等使用場景,POST仍是會比GET更安全。最後說了幾個經常使用的http狀態碼,並用一些實際的例子加深印象和理解。

相關文章
相關標籤/搜索