本篇梳理 HTTP 的相關知識,也分享個人學習經驗。有認識不對的地方,歡迎指正!
前端開發的很多工作,似乎都是 “所見即所得” 的,這種對知識強大的 “既視感”,是前端容易入門的一個關鍵因素。但越往后走,一些底層而重要的知識,就沒那么容易觸及了,比如 HTTP
協議。
一開始,可能就只知道 Ajax
是瀏覽器端 HTTP
強相關的一套 API;另外,無論是瀏覽器端,還是服務器端,都有一套對 HTTP
的解析、處理的機制。HTTP
可以說是前、后端通信的載體。在我們工作中、或者我們面試時,經常涉及的性能優化問題、跨域問題、安全問題等等,都和它緊密相關。
就是因為它更底層,邏輯對外不可見,知識的 “既視感” 也比較難實現(如果有哪個團隊做一款這樣的產品,那肯定會在前端圈里圈粉無數)。但無論如何,HTTP
是前端進階應該攻破的一道坎。
盡管它不像頁面 DOM
元素、或者一個 react
組件那樣觸手可及,但我們仍然應該有辦法學習它。我們可以暫且不理會內幕細節,把它當做一個 “黑盒子”,而通過一些其他的途徑,旁敲側擊。比如使用一些抓包工具(本文用的是 Fiddler ),也能很好的窺視 HTTP
的形貌。
1、認識 HTTP 報文
以訪問“簡書”站點首頁http://www.lxweimin.com/
為例,通過 Fiddler 工具抓取到以下報文信息。
請求報文
GET http://www.lxweimin.com/ HTTP/1.1
Host: www.lxweimin.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 Chrome/58.0.3029.110 Safari/537.36
Accept: text/html;q=0.9,image/webp,*/*;q=0.8
Referer: http://www.lxweimin.com
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
//本行及以下均為報文主體
無論請求報文,還是響應報文,都是有著嚴格的格式規范的,它們絕不是一堆毫無規范組織的字符串。正式這一套標準規范,讓報文的解析、分割、組包可以用一定的算法實現,具體內幕本文不涉及。
響應報文
HTTP/1.1 200 OK
Date: Mon, 14 Aug 2017 12:36:22 GMT
Server: Tengine
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/"fffd6ab16cec9ff5e97d58a4d293073c"
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: _session_id=--4007a37778519a153aad82b63--; path=/; HttpOnly
X-Request-Id: 9c7aa92b-99b8-4e14-a87a-012d88e40608
X-Runtime: 0.230320
X-Via: 1.1 edianxin19:8 (Cdn Cache Server V2.0)
Connection: keep-alive
Content-Length: 56884
<!DOCTYPE html>
<!--[if IE 6]><html class="ie lt-ie8"><![endif]-->
<!--[if IE 7]><html class="ie lt-ie8"><![endif]-->
<!--[if IE 8]><html class="ie ie8"><![endif]-->
//余下報文主體...
HTTP
報文,基本還是符合 “所見即多得” 的,因為它包含的內容的的確確就是這樣的。
值得強調的是 HTTP
報文的 “結構化”,是一個非常重要的特征,尤其是報文頭部項,不僅是下文重點關注的對象,也是今后學習的一大重點。下圖,以請求報文為例,再次感受這種 “結構化”。
2、HTTP 的相關概念
兩臺計算機之間的通信是通過 TCP/IP
協議在因特網上進行的。可以借用一個 20 多年前的例子來理解,比如兩個好友用信件聯絡,他們能建立通信的條件,一個是互相明確彼此的郵箱地址(IP
),然后還需要一個郵差去分類和送達(TCP
)。
IP: Internet Protocol 網際協議。每臺計算機都有一個·IP
,用來在 internet 上標識這臺計算機。IP
協議負責將計算機之間傳輸的每個數據包路由至它的目的地。
TCP:Transmission Control Protocol 傳輸控制協議。顧名思義,是傳輸控制。TCP
負責在數據傳送之前將它們分割為數據包,然后在它們到達的時候將它們重組,確保數據包以正確的次序到達。
HTTP 協議采用了請求/響應模型??蛻舳讼蚍掌靼l送一個請求報文,請求報文包含請求的方法、URL、協議版本、請求頭部(header
)和請求體(body
)數據。
服務器以一個狀態行作為響應,響應的內容包括協議的版本、成功或者錯誤代碼、服務器信息、響應頭部(header
)和響應體(body
)數據。
HTTP 是應用層協議,是基于 TCP/IP 協議之上的一種應用。作為應用層的 HTTP 協議,通信過程依次會穿越傳輸層、網絡層、鏈路層。詳見下圖:
3、縮小關注范圍
毫不諱言,學習 HTTP
比較麻煩,除了它不像前端的其他領域知識那樣 “既視感”強之外,還有就是面對一個理論性很強的知識,往往很難找到 “下口” 的地方,更別說輕易消化。而在一個信息泛濫的時代,閱后即焚、收集癖這些 “淺閱讀” 形式更是我們深入掌握一些知識的頭號敵人。
通常,對于獲取知識,個人經驗來看有這些層次:
- 最淺層次莫過于瀏覽、收藏
- 次淺層次是復制、截圖、粘貼成筆記;
- 速成(鼓吹)的方式是聽課看視頻;
- 較深層次則是整理知識、梳理結構,畫原理圖、思維導圖;
- 更為深層而牢靠的則是實踐、實操中Q&A和技術交流。
我們很難掌握某些知識,包括筆者親身體會,就是大部分都停留在第 3 階段,很少越過第 4 階段。
到了第 4 階段,就應該去劃分邊界,縮小關注范圍,然后重點突破。就好比本文的梳理,至少可以明確,現在(及今后一段時間)的注意力,就完全可以集中在 HTTP
頭部項及其包含的實現原理、客戶端的 API (Ajax
)、服務端的 API (完全可以通過 Nodejs 模塊之http.js
來學習)。
其中,HTTP
頭部項,可以慢慢積累,這里暫且不談。本文下部分主要探討通過客戶端的 Ajax
來了解瀏覽器和服務器間的 HTTP
通信是怎樣的。
瀏覽器端調試 HTTP
以一個問題開始
當客戶端請求被 abort
(取消)掉后,瀏覽器和服務器分別會如何處理這個請求?
這是個好問題,它立刻激發我的探究欲望。對于這個問題的驗證,只需用上兩個工具——瀏覽器和 Fiddler 抓包工具——就能搭建一個很好的驗證環境。其中,瀏覽器中運行的 JavaScript
腳本非常簡單,如下:
var xhr = new XMLHttpRequest();
xhr.open("GET", 'http://www.vrstarman.com/stack.html', true);
//狀態變更
xhr.onreadystatechange = function(e){
console.log('請求狀態值變更:', xhr.readyState);
if(xhr.readyState == '2'){
//xhr.abort();
}
};
//進行中
xhr.onprogress = function(e){
console.log('請求進行時狀態:', xhr.readyState);
};
//中斷
xhr.onabort = function(e){
console.log('請求取消事件:', e', '此時狀態值:', xhr.readyState);
};
//加載完成
xhr.onload = function(e){
console.log('請求完成事件:', e);
};
//錯誤
xhr.onerror = function(){
console.log('請求錯誤時狀態:', xhr.readyState);
};
xhr.send();
問題的變量因素有:
- 在不同狀態階段
readyState
階段abort
掉請求; - 在程序的不同的位置執行
abort
; - 在這些
abort
測試中,驗證的url
分別為以下情形:- [v] 一個實際存在的 url
- [ ] 一個不存在的 url
- [ ] 一個跨域的 url
本文就用簡書
的首頁地址http://www.lxweimin.com/
測試了第一種情形,并且已經收獲頗豐??梢酝茰y第二種情形和第一種差不多,但第三種會不同,非常值得驗證。
驗證結果及分析
readyState | 狀態含義解析 | abort 位置 |
---|---|---|
0 | 請求未初始化, 即 open() 前的狀態 |
此時 abord ,后續方法拋出錯誤 |
1 | 請求已經建立,但是還未send() ,此時打印狀態是1。 send() 之后打印狀態,結果還是1 (按理說應該是 2 ),可見狀態變更是滯后的,說明是異步的 |
在 open() 之后 send() 之前 abord ,會報錯,故用try{ xhr.send(); }catch(e){ }捕捉錯誤
|
2 | 請求已send() ,正在處理中,并已經接收到響應數據(通常瀏覽器可從響應中解析出請求頭、響應頭) |
鑒于異步執行,將 xhr.abord() 放在 onreadystatechange 事件中 |
3 | 響應數據仍在接收和處理中,響應 body 已部分解析,但服務器還未全部發送完或瀏覽器還未全部解析完 |
在 readyState == 3 下 abord() ,由于異步執行的原因,實際請求已接近(或達到)狀態 4
|
4 | 響應已完成接收和解析,可獲取并使用所有響應內容 | 從以上操作看,一個請求,只要不在狀態 2 之前取消,都會經歷完整的響應階段,即完成狀態 4
|
進一步詳細解讀
達到了不同的狀態值,瀏覽器和服務器都分別做了哪些動作,以下是上述探索的詳細解讀。
狀態為 0
狀態為
0
是被大多瀏覽器隱藏的。只在瀏覽器端new
出了xhr
對象,但還未建立連接就中斷請求是沒有意義的。
狀態為 1
結果是無任何請求發出,且 打印的
readyState
為0
。
為什么還是0
?雖然open()
已執行,但因立即打印狀態,還沒有收到已建立連接的返回消息,故立即打印的狀態是變更前的。
Fiddler 此時未顯示任何http
請求。
瀏覽器顯示Provisional headers are shown
,這與瀏覽器的可視化機制有關,真實情況瀏覽器并未發送請求。
狀態為 2
readyState
狀態值變更滯后(因為異步),給判斷帶來干擾
但測試表明放在onreadystatechange
事件中的abord()
,會導致readyState
值從2
“跳” 到4
。
瀏覽器已成功發送請求,并成功的解析了請求頭、響應頭。
Fiddler 監測出了http
請求,表明服務器已成功發送請求數據。
但因為手動abord()
,導致瀏覽器接收到了(部分甚至可能全部)響應但放棄了解析響應的body
,所以頁面無數據顯示。
狀態為 3
盡管是中途
abord()
,但readyState
完整經歷4
個狀態變化。
onprogress
事件只激發了2
次。
瀏覽器處于一個不確定是否完全解析響應的狀態中,反復測試并打印xhr.response
會發現有時解析了完整的數據,有時則在中間產生了中斷。
服務器已經確定發出了(幾乎)所有響應內容。
由于在狀態4
前中斷,瀏覽器未確切是否已完成全部接收和解析,故在瀏覽器控制臺的 Preview 中無法看到內容。
狀態為 4
abord
會導致xhr.response
和xhr.responseText
被清空。
服務器端全部響應也發送完畢。
瀏覽器已經全部接受、并完成解析,這在瀏覽器控制臺的 Preview中能看到。
但還是因為abord()
,導致了對響應body
內容的情況,所以還是無法最終顯示在document
中。除非在abord()
前執行document.write(xhr.response)
;
其中,對 readyState == 2
即 abord()
的情形,服務器到底有沒有響應,若有,響應了什么內容?因為從瀏覽器的控制臺無從確認,對于這一步的存疑,還好可以借助 Fiddler 的攔截,一目了然。
對于跨域的 url
,在下不同 HTTP
請求階段被 abord
情況的探索,就暫不描述了。強烈建議讀者一并探索下。
另外,HTTP
頭部項、以及 HTTP
服務端的一些特性的學習,后續找時間再寫。