本文作者:饒全成,中科院計算所碩士,滴滴出行後端研發工程師。
上一篇文章中,我們學會了用wireshark和tcpdump來分析TCP的“三次握手,四次揮手”,非常好用。這哥倆就是傳說中的 錘子
,拿着 錘子
,看什麼都像 釘子
!在這篇文章中,我對準了 HTTP
這顆釘子砸下去,咳咳。
爲了對網絡數據包的“流轉”有更加深刻的理解,我在docker(遠程)上部署一個服務,支持http方式調用。從客戶端(本地)用http方式請求其中的一個接口,並得到響應數據。同時本地通過wireshark抓包,遠程用tcpdump抓包,然後分析過程中的所有通信細節。悲劇是把美好的東西撕碎給人看,而我則是把複雜的東西撕碎了給人看。
文章稍長,請在看本文時保持耐心。我先通過工具獲取HTTP通信的數據包,再來抽絲剝繭,深入二進制的天地裏,解密HTTP所有的通信細節。分析過程中,由點到面,將相關知識串接起來。保證全篇讀完之後,你對HTTP的理解會上升一個臺階!
爲了更好的閱讀體驗,我手動貼上本文的目錄:
HTTP報文截獲
背景介紹
我手頭現在有一個地理幾何相關的服務,它提供一組接口對外使用。其中有一個接口是 Fence2Area
. 使用方傳入一個圍欄(由點的列表組成,點由<經度,緯度>表示)、點的座標系類型(谷歌地圖用的是wgs84, 國內騰訊、高德用的是soso, 而百度用的是另一套自己的座標系),接口輸出的則是圍欄的面積。
我請求服務的“Fence2Area”接口,輸入圍欄(fence)頂點(lng, lat)座標、座標系類型(coordtype),輸出的則是多邊形的面積(area).
一次正常的請求示例url, 這個大家都不陌生(我用docker_ip代替真實的ip):
http://docker_ip:7080/data?cmd=Fence2Area&meta={"caller":"test","TraceId":"test"}&request={"fence":[{"lng":10.2,"lat":10.2}, {"lng":10.2,"lat":8.2}, {"lng":8.2,"lat":8.2}, {"lng":8.2,"lat":10.2}],"coordtype":2}
請求發出後,服務器進行處理,之後,客戶端收到返回的數據如下:
{ "data": { "area": 48764135597.842606 }, "errstr": ""}
area
字段表示面積, errstr
表示出錯信息,空說明沒有出錯。
抓包
在真正發送請求之前,需要進行抓包前的設置。在本地mac,我用wireshark; 而在遠程docker上,我用tcpdump工具。
mac本地
設置wireshark包過濾器,監控本地主機和遠程docker之間的通信。
ip.addr eq docker_ip
點擊開始捕獲。
遠程docker
該服務通過7080端口對外提供,使用如下命令捕獲網絡包:
tcpdump -w /tmp/testHttp.cap port 7080 -s0
請求 && 分析
準備工作做完,我選了一個神聖的時刻,在本地通過瀏覽器訪問如下url:
http://docker_ip:7080/data?cmd=Fence2Area&meta={"caller":"test","TraceId":"test"}&request={"fence":[{"lng":10.2,"lat":10.2}, {"lng":10.2,"lat":8.2}, {"lng":8.2,"lat":8.2}, {"lng":8.2,"lat":10.2}],"coordtype":2}
這樣本地的wireshark和遠程的tcpdump都能抓取到HTTP網絡數據包。
關閉服務進程
正式請求之前,我們先看一下幾種特殊的情形。
首先,關閉gcs服務進程,請求直接返回RST報文。
如上圖,我在請求的時候,訪問服務端的另一個端口 5010
, 這個端口沒有服務監聽,和關閉gcs服務進程是同樣的效果。可以看到,客戶端發送SYN報文,但直接被遠程docker RST掉了。因爲服務端操作系統找不到監聽此端口的進程。
關閉docker
關閉docker, 由於發送的SYN報文段得不到響應,因此會進行重試,mac下重試的次數爲10次。
先每隔1秒重試了5次,再用“指數退避”的時間間隔重試,2s, 4s, 8s, 16s, 32s. 最後結束。
重啓docker
先進行一次正常的訪問,隨後重啓docker。並再次在本地訪問以上url, 瀏覽器這時還是用的上一次的端口,訪問到服務端後,因爲它已經重啓了,所以服務端已經沒有這個連接的消息了。因此會返回一個RST報文。
正常請求
服務正常啓動,正常發送請求,這次請求成功,那是當然的,嘿嘿!
這是在mac上用wireshark捕獲的數據包,共7個包,前三個包爲3次握手的包,第四個包爲 HTTP
層發送的請求數據,第五個包爲服務端的TCP 確認報文,第六個包爲服務端在 HTTP
層發送的響應數據,第七個包爲mac對第六個包的確認報文。
重點來關注後面幾個包,先看第四個包,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
我們來逐字節分析。
剩餘的數據部分即爲TCP協議相關的。TCP也是20B固定長度+可變長度部分。
可變長度部分,協議如下:
剩下來的就是數據部分了。我們一行一行地看。因爲http是字符流,所以我們先看一下ascii字符集,執行命令:
man ascii
可以得到ascii碼,我們直接看十六進制的結果:
把上表的最後一列連起來,就是:
GET /data?cmd=Fence2Area&meta={%22caller%22:%22test%22,%22TraceId%22:%22test%22}&request={%22fence%22:[{%22lng%22:10.2,%22lat%22:10.2},%20{%22lng%22:10.2,%22lat%22:8.2},%20{%22lng%22:8.2,%22lat%22:8.2},%20{%22lng%22:8.2,%22lat%22:10.2}],%22coordtype%22:2} HTTP/1.1 Host: 10.96.92.212:7080 Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.2 Safari/605.1.15 Accept-Language: zh-cn Accept-Encoding: gzip, deflate Connection: keep-alive
其中,cr nl表示回車,換行。
docker收到數據後,會回覆一個ack包。第四個包的總長度爲661字節,去掉IP頭部20字節,TCP頭部固定部分20字節,TCP頭部可選長度爲12字節,共52字節,因此TCP數據部分總長度爲661-52=609字節。另外,序列號爲2778351310.
再來看第5個包,字節流如下:
1 2 3 4 |
|
剩餘的數據部分即爲TCP協議相關的。TCP也是20B固定長度+可變長度部分。
可變長度部分,協議如下:
數據部分爲空,這個包僅爲確認包。
再來看第六個包,字節流如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
剩餘的數據部分即爲TCP協議相關的。TCP也是20B固定長度+可變長度部分。
可變長度部分,協議如下:
剩下來的就是數據部分了。我們一行一行地看。
把上表的最後一列連起來,就是:
1 2 3 4 5 6 |
|
Content-Length: 48,最後一行的長度即爲48個字節。
最後,第七個包,字節流如下:
1 2 3 4 |
|
剩餘的數據部分即爲TCP協議相關的。TCP也是20B固定長度+可變長度部分。
可變長度部分,協議如下:
至此,一次完整的http請求的報文就解析完了。感覺如何,是不是很親切?
HTTP協議分析
上面我們把HTTP協議相關的數據給解構了,下面我將對照上面的數據拆解結果,一步步帶你深入理解HTTP協議。
整體介紹
HTTP
(Hypertext Transfer Protocol)超文本傳輸協議,是在互聯網上進行通信時使用的一種協議。說得更形象一點: HTTP
是現代互聯網中使用的公共語言。它最著名的應用是用在瀏覽器的服務器間的通信。
HTTP屬於應用層協議,底層是靠TCP進行可靠地信息傳輸。
HTTP在傳輸一段報文時,會以 流
的形式將報文數據的內容通過 一條打開
的TCP連接按序傳輸。TCP接到上層應用交給它的數據流之後,會按序將數據流打散成一個個的分段。再交到IP層,通過網絡進行傳輸。另一端的接收方則相反,它們將接收到的分段按序組裝好,交給上層HTTP協議進行處理。
編碼
我們再來回顧一下:
原始的url值:
/data?cmd=Fence2Area&meta={"caller":"test","TraceId":"test"}&request={"fence":[{"lng":10.2,"lat":10.2}, {"lng":10.2,"lat":8.2}, {"lng":8.2,"lat":8.2}, {"lng":8.2,"lat":10.2}],"coordtype":2}
編碼後的url值:
/data?cmd=Fence2Area&meta={%22caller%22:%22test%22,%22TraceId%22:%22test%22}&request={%22fence%22:[{%22lng%22:10.2,%22lat%22:10.2},%20{%22lng%22:10.2,%22lat%22:8.2},%20{%22lng%22:8.2,%22lat%22:8.2},%20{%22lng%22:8.2,%22lat%22:10.2}],%22coordtype%22:2}
在之前的報文拆解過程中,我們看到多了很多 %22
,其實, 0x22
是單引號 "
的ascii值,
一方面,URL描述的資源爲了能通過其他各種協議傳送,但是有些協議在傳輸過程中會剝去一些特定的字符;另一方面,URL還是可讀的,所以那些不可打印的字符就不能在URL中使用了,比如空格;最後,URL還得是完整的,它需要支持所有語言的字符。
總之,基於很多原因,URL設計者將US-ASCII碼和其轉義序列集成到URL中,通過轉義序列,就可以用US-ASCII字符集的有限子集對任意字符或數據進行編碼了。
轉義的方法:百分號( %
)後跟着兩個表示ASCII碼的十六進制數。比如:
所以上面在瀏覽器發送給服務器的URL進行了非“安全字符”編碼,也就不奇怪了吧?
在URL中,當上面的保留字符用在保留用途之外的場合時,需要對URL進行編碼。
MIME類型
響應數據中,我們注意到有一個首部:
Content-Type: text/plain; charset=utf-8
互聯網上有數千種不同的數據類型,HTTP給每種對象都打上了MIME(Multipurpose Internet Media Extension, 多用途因特網郵件擴展)標籤,也就是響應數據中的 Content-Type
. MIME本來是用在郵件協議中的,後來被移植到了HTTP中。瀏覽器從服務器上取回了一個對象時,會去查看MIME類型,從而得知如何處理這種對象,是該展示圖片,還是調用聲卡播放聲音。MIME通過斜槓來標識對象的主類型和其中的特定的子類型,下表展示了一些常見的類型,其中的實體主體是指body部分:
URI/URL/URN
URI(Uniform Resource Identifier, 統一資源標識符)表示服務器資源,URL(Uniform Resource Locator, 統一資源定位符)和URN(Uniform Resource Name, 統一資源名)是URI的具體實現。URI是一個通用的概念,由兩個主要的子集URL和URN構成,URL通過位置、URN通過名字來標識資源。
URL定義了資源的位置,表示資源的實際地址,在使用URL的過程中,如果URL背後的資源發生了位置移動,訪問者就找不到它了。這個時候就要用到URN了,它給定資源一個名字,無論它移動到哪裏,都可以通過這個名字來訪問到它,簡直完美!
URL通常的格式是:
協議方案+服務器地址+具體的資源路徑
協議方案(scheme),如 http
, ftp
,告知web客戶端怎樣訪問資源);服務器地址,如 www.oreilly.com
; 具體的資源路徑,如 index.html
.
HTTP方法
HTTP支持幾種不同的請求方法,每種方法對服務器要求的動作不同,如下圖是幾種常見的方法:
HEAD方法只獲取頭部,不獲取數據部分。通過頭部可以獲取比如資源的類型(Content-Type)、資源的長度(Content-Length)這些信息。這樣,客戶端可以獲取即將請求資源的一些情況,可以做到心中有數。
POST用於向服務器發送數據,常見的是提交表單;PUT用於向服務器上的資源存儲數據。
狀態碼
每條HTTP的響應報文都會帶上一個三位數字的狀態碼和一條解釋性的“原因短語”,通知客戶端本次請求的狀態,幫助客戶端快速理解事務處理結果,最常見的是:
200 OK 404 Not Found 500 Internal Server Error
我們平時使用瀏覽器的時候,很多的錯誤碼其實是由瀏覽器處理的,我們感知不到。但是 404NotFound
會穿透重重迷霧,來到我們面前,爲何?那是因爲他對我們愛的深沉啊!
客戶端可以據此狀態碼,決定下一步的行動(如重定向等)。
三位數字的第一位表示分類:
報文格式
HTTP報文實際上是由一行行的字符串組成的,每行字符串的末尾用 \r\n
分隔,人類可以很方便的閱讀。順便說一句,不是所有的協議都對人類這麼友好的,像thrift協議,直接甩一堆字節給你,告訴你說 0x0001
表示調用方法,諸如此類的,你只能對着一個十六進制的數據塊一個個地去“解碼”。不可能像HTTP協議這樣,直接將字符編碼,人類可以直接讀懂。
舉個簡單的請求報文和響應報文的格式的例子:
實際上,請求報文也是可以有body(主體)部分的。請求報文是由 請求行(request line)、請求頭部(header)、空行、請求數據
四個部分組成。唯一要注意的一點就是,請求報文即使body部分是空的,請求頭部後的 回車換行
符也是必須要有的。
響應報文的格式和請求報文的格式類似:
請求報文、響應報文的起始行和響應頭部裏的字段都是文本化、結構化的。而請求body卻可以包含任意二進制數據(如圖片、視頻、軟件等),當然也可以包含文本。
有些首部是通用的,有些則是請求或者響應報文纔會有的。
順便提一下, 用telnet直連服務器的http端口,telnet命令會建立一條TCP通道,然後就可以通過這個通道直接發送HTTP請求數據,獲取響應數據了。
HTTP協議進階
代理
HTTP的代理服務器既是Web服務器,又是Web客戶端。
使用代理可以“接觸”到所有流過的HTTP流量,代理可以對其進行監視和修改。常見的就是對兒童過濾一些“成人”內容;網絡工程師會利用代理服務器來提高安全性,它可以限制哪些應用層的協議數據可以通過,過濾“病毒”等數據;代理可以存儲緩存的文件,直接返回給訪問者,無需請求原始的服務器資源;對於訪問慢速網絡上的公共內容時,可以假扮服務器提供服務,從而提高訪問速度;這被稱爲 反向代理
;可以作爲內容路由器,如對付費用戶,則將請求導到緩存服務器,提高訪問速度;可以將頁面的語言轉換到與客戶端相匹配,這稱爲 內容轉碼器
; 匿名代理
會主動從HTTP報文中刪除身份相關的信息,如 User-Agent
, Cookie
等字段。
現實中,請求通過以下幾種方式打到代理服務器上去:
報文每經過一箇中間點(代理或網關),都需要在首部via字段的末尾插入一個可以代表本節點的獨特的字符串,包含實現的協議版本和主機地址。注意圖中的via字段。
請求和響應的報文傳輸路徑通常都是一致的,只不過方向是相反的。因此,響應報文上的via字段表示的中間節點的順序是剛好相反的。
緩存
當有很多請求訪問同一個頁面時,服務器會多次傳輸同一份數據,這些數據重複地在網絡中傳輸着,消耗着大量帶寬。如果將這些數據緩存下來,就可以提高響應速度,節省網絡帶寬了。
大部分緩存只有在客戶端發起請求,並且副本已經比較舊的情況下才會對副本的新鮮度進行檢測。最常用的請求首部是 If-Modified-Since
, 如果在xx時間(此時間即爲If-Modified-Since的值)之後內容沒有變化,服務器會迴應一個 304NotModified
. 否則,服務器會正常響應,並返回原始的文件數據,而這個過程中被稱爲 再驗證命中
。
再驗證可能出現命中或未命中的情況。未命中時,服務器回覆 200OK
,並且返回完整的數據;命中時,服務器回覆 304NotModified
; 還有一種情況,緩存被刪除了,那麼根據響應狀態碼,緩存服務器也會刪除自己緩存的副本。
順帶提一句,若要在項目中使用緩存,就一定要關注緩存命中比例。若命中比例不高,就要重新考慮設置緩存的必要性了。
緩存服務器返回響應的時候,是基於已緩存的服務器響應的首部,再對一些首部字段做一些微調。比如向其中插入新鮮度信息(如 Age
, Expires
首部等),而且通常會包含一個 via
首部來說明緩存是由一個緩存代理提供的。注意,這時不要修改 Date
字段,它表示原始服務器最初構建這條響應的日期。
HTTP通過 文檔過期機制
和 服務器再驗證機制
保持已緩存數據和服務器間的數據充分一致。
文檔過期通過如下首部字段來表示緩存的有效期:
當上面兩個字段暗示的過期時間已到,需要向服務器再次驗證文檔的新鮮度。如果這時緩存仍和服務器上的原始文檔一致,緩存只需要更新頭部的相關字段。如上表中提到的 Expires
字段等。
爲了更好的節省網絡流量,緩存服務器可以通過相關首部向原始服務器發送一個 條件GET
請求, 這樣只有在緩存真正過期的情況下,纔會返回原始的文檔,否則只會返回相關的首部。 條件GET
請求會用到如下的字段:
cookie
cookie是服務器“貼在”客戶端身上的標籤,由客戶端維護的狀態片段,並且只會回送給合適的站點。
有兩類cookie: 會話cookie、持久cookie. 會話cookie在退出瀏覽器後就被刪除了;而持久cookie則保存在硬盤中,計算機重啓後仍然存在。
服務器在給客戶端的響應字段首部加上 Set-cookie
或 Set-cookie2
, 值爲 名字=值
的列表,即可以包含多個字段。當下次瀏覽器再次訪問到相同的網站時,會將這些字段通過 Cookie
帶上。cookie中保留的內容是服務器給此客戶端打的標籤,方便服務進行追蹤的識別碼。瀏覽器會將cookie以特定的格式存儲在特定的文件中。
瀏覽器只會向產生這條cookie的站點發生cookie. Set-cookie
字段的值會包含 domain
這個字段,告知瀏覽器可以把這條cookie發送給給相關的匹配的站點。 path
字段也是相似的功能。如i瀏覽器收到如下的cookie:
Set-cookie: user="mary"; domain="stefno.com"
那麼瀏覽器在訪問任意以 stefno.com
結尾的站點都會發送:
Cookie: user="mary"
實體和編碼
響應報文中的body部分傳輸的數據本質上都是二進制。我們從上面的報文數據也可以看出來,都是用十六進制數來表示,關鍵是怎麼解釋這塊內容。如果 Content-Type
定義是 text/plain
, 那說明body內容就是文本,我們直接按文本編碼來解釋;如果 Content-Type
定義是 image/png
, 說明body部分是一幅圖片,那我們就按圖片的格式去解釋數據。
Content-Length
標示報文主體部分的數據長度大小,如果內容是壓縮的,那它表示的就是壓縮後的大小。另外, Content-Length
在長連接的情況下,可以對多個報文進行正確地分段。所以,如果沒有采用分塊編碼,響應數據中必須帶上 Content-Length
字段。分塊編碼的情形中,數據被拆分成很多小塊,每塊都有大小說明。因此,任何帶有主體部分的報文(請求或是響應)都應帶上正確的 Content-Length
首部。
HTTP的早期版本採用關閉連接的方式來劃定報文的結束。這帶來的問題是顯而易見的:客戶端並不能分清是因爲服務器正常結束還是中途崩潰了。這裏,如果是客戶端用關閉來表示請求報文主體部分的結束,是不可取的,因爲關閉之後,就無法獲取服務器的響應了。當然,客戶端可以採用半關閉的方式,只關閉數據發送方向,但是很多服務器是不識別的,會把半關閉當成客戶端要成服務器斷開來處理。
HTTP報文在傳輸的過程中可能會遭到代理或是其他通信實體的無意修改,爲了讓接收方知道這種情況,服務器會對body部分作一個md5, 並把值放到 Content-MD5
這個字段中。但是,如果中間的代理即修改了報文主體,又修改了md5, 就不好檢測了。因此規定代理是不能修改 Content-MD5
首部的。這樣,客戶端在收到數據後,先進行解碼,再算出md5, 並與 Content-MD5
首部進行比較。這主要是防止代理對報文進行了無意的改動。
HTTP在發送內容之前需要對其進行編碼,它是對報文主體進行的可逆變換。比如將報文用gzip格式進行壓縮,減少傳輸時間。常見的編碼類型如下:
當然,客戶端爲了避免服務器返回自己不能解碼的數據,請求的時候,會在 Accept-Encoding
首部裏帶上自己支持的編碼方式。如果不傳輸的話,默認可以接受任何編碼方式。
上面提到的編碼是內容編碼,它只是在響應報文的主體報文將原始數據進行編碼,改變的是內容的格式。還有另一種編碼: 傳輸編碼
。它與內容無關,它是爲了改變報文數據在網絡上傳輸的方式。傳輸編碼是在HTTP 1.1中引入的一個新特性。
通常,服務器需要先生成數據,再進行傳輸,這時,可以計算數據的長度,並將其編碼到 Content-Length
中。但是,有時,內容是動態生成的,服務器希望在數據生成之前就開始傳輸,這時,是沒有辦法知道數據大小的。這種情況下,就要用到 傳輸編碼
來標註數據的結束的。
HTTP協議中通過如下兩個首部來描述和控制傳輸編碼:
分塊編碼的報文形式是這樣的:
每個分塊包含一個長度值(十六進制,字節數)和該分塊的數據。 <CR><LF>
用於區隔長度值和數據。長度值不包含分塊中的任何 <CR><LF>
序列。最後一個分塊,用長度值0來表示結束。注意報文首部包含一個 Trailer:Content-MD5
, 所以在緊跟着最後一個報文結束之後,就是一個拖掛。其他如, Content-Length
, Trailer
, Transfer-Encoding
也可以作爲拖掛。
內容編碼和傳輸編碼是可以結合起來使用的。
國際化支持
HTTP爲了支持國際化的內容,客戶端要告知服務器自己能理解的何種語言,以及瀏覽器上安裝了何種字母表編碼算法。這通過 Accept-Charset
和 Accept-Language
首部實現。
比如:
Accept-Language: fr, en;q=0.8Accept-Charset: iso-8859-1, utf-8
表示:客戶端接受法語(fr, 優先級默認爲1.0)、英語(en, 優先級爲0.8),支持iso-8859-1, utf-8兩種字符集編碼。服務器則會在 Content-Type
首部裏放上 charset
.
本質上,HTTP報文的body部分存放的就是一串二進制碼,我們先把二進制碼轉換成字符代碼(如ascii是一個字節表示一個字符,而utf-8則表示一個字符的字節數不定,每個字符1~6個字節),之後,用字符代碼去字符集中找到對應的元素。
比較常見的字符集是 US-ASCII
: 這個字符集是所有字符集的始祖,早在1968年就發佈了標準。ASCII碼的代碼值從0到127, 只需要7個bit位就可以覆蓋代碼空間。HTTP報文的首部、URL使用的字符集就是ASCII碼。可以再看下上文報文分析部分的acsii碼集。
US-ASCII
是把每個字符編碼成固定的7位二進制值。 UTF-8
則是無固定的編碼方案。第一個字節的高位用來表示編碼後的字符所用的字節數(如果所用的字節數是5,則第一個字節前5bit都是1,第6bit是0),所需的後續的字節都含有6位的代碼值,前兩個bit位是用 10
標識。
舉個例子,漢字“嚴”的Unicode編碼爲 4E25
( 100111000100101
), 共有15位,落在上表中的第三行,因此“嚴”的編碼就需要三個字節。將 100111000100101
填入上表中的 c
位即可。因此,嚴的 UTF-8
編碼是11100100 10111000 10100101,轉換成十六進制就是E4B8A5. 比如我在谷歌搜索框裏搜索“嚴”字,google發出的請求如下:
https://www.google.com.hk/search?q=%E4%B8%A5&oq=%E4%B8%A5&aqs=chrome..69i57j0l5.3802j0j4&sourceid=chrome&ie=UTF-8&gws_rd=cr
q=%E4%B8%A5
這個就是搜索的詞了。
重定向與負載均衡
Web內容通常分散地分佈在很多地方,這可以防止“單點故障”,萬一某個地方發生地震了,機房被毀了,那還有其他地方的機房可以提供服務。一般都會有所謂的“雙活”,“多活”,所謂 狡兔三窟
嘛。
這樣,用戶的請求會根據 負載均衡
的原則,被 重定向
到它應該去的地方。
HTTP重定向
服務器收到客戶端請求後,向客戶端返回一條帶有狀態碼 302
重定向的報文,告訴他們應該去其他的地方試試。web站點將重定向看成一種簡單的負載均衡策略來使用, 重定向
服務器找到可用的負載最小的機器,由於服務器知道客戶端的地址,理論上來說,可以做到最優的重定向選擇。
當然,缺點也是顯而易見的,由於客戶端要發送兩次請求,因此會增加耗時。
DNS重定向
DNS將幾個IP地址關聯到一個域上,採用算法決定返回的IP地址。可以是簡單的 輪轉
;也可以是更高級的算法,如返回負載最輕的服務器的IP地址,稱爲 負載均衡算法
;如果考慮地理位置,返回給客戶端最近位置的地址,稱爲 鄰接路由算法
;還有一種是繞過出現故障的地址,稱爲 故障屏蔽算法
。
DNS服務器總是會返回所有的IP地址,但是DNS客戶端一般只會使用第一個IP地址,而且會緩存下來,之後會一直用這個地址。所以,DNS輪轉通常不會平衡單個客戶端的負載。但是,由於DNS服務器對於不同的請求,總是會返回輪轉後的IP地址列表,因此,會把負載分散到多個客戶端。
HTTP連接
HTTP連接是HTTP報文傳輸的關鍵通道。
並行連接
對於一個頁面上同時出現多個對象的時候,如果瀏覽器並行地打開多個連接,同時去獲取這些對象,多個連接的TCP握手時延可以進行重疊,速度會快起來。
如一個包含3張圖片的頁面,瀏覽器要發送4次HTTP請求來獲取頁面。1個用於頂層的HTML頁面,3個用於圖片。如果採用串行方式,那麼連接時延會進行疊加。
採用並行連接之後:
但是並行連接也不絕對提升速度,如果一個頁面有數百個內嵌對象,那要啓動數百個連接,對服務器的性能也是非常大的挑戰。所以,通常瀏覽器會限制並行連接的總數據在一個較小的值,通常是4個,而且服務端可以隨意關閉客戶端超量的連接。
另一方面,如果客戶端網絡帶寬較小,每個連接都會去爭搶有限的帶寬,每個連接都會獲取較小的速度,即每個對象都會以較小的速度去加載。這樣,並行連接帶來的速度提升就會比較小,甚至沒有提升。
持久連接
HTTP keep-alive機制
我們知道HTTP請求是“請求-應答”模式,每次請求-應答都要新建一個連接,完成之後要斷開連接。HTTP是無狀態的,連接之間沒有任何關係。
HTTP是應用層協議,TCP是傳輸層協議。HTTP底層仍然採用TCP進行傳輸數據。TCP爲HTTP提供了一層可靠的比特傳輸通道。HTTP一般交換的數據都不大,而每次連接都要進行TCP三次握手,很大一部分時間都消耗在這上面,有時候甚至能達到50%。如果能複用連接,就可以減少由於TCP三次握手所帶來的時延。
HTTP 1.1默認開啓keep-alive機制,從上面抓到的包也可以看到。這樣,數據傳輸完成之後保持TCP連接不斷開,之後同域名下複用連接,繼續用這個通道傳輸數據。服務器在響應一個請求後,可以保持這個連接keep-alive timeout的時間,在這個時間內沒有請求,則關閉此連接;否則,重新開始倒計時keep-alive timeout時間。
HTTP有keep-alive機制,目的是可以在一個TCP 連接上傳輸多個HTTP事務,以此提高通信效率。底層的TCP其實也有keep-alive機制,它是爲了探測TCP連接的活躍性。TCP層的keepalive可以在任何一方設置,可以是一端設置、兩端同時設置或者兩端都沒有設置。新建socket的時候需要設置,從而使得協議棧調用相關函數tcpsetkeepalive,來激活連接的keep-alive屬性。
當網絡兩端建立了TCP連接之後,閒置(雙方沒有任何數據流發送往來)時間超過 tcp_keepalive_time
後,服務器內核就會嘗試向客戶端發送偵測包,來判斷TCP連接狀況(有可能客戶端崩潰、強制關閉了應用、主機不可達等等)。如果沒有收到對方的回答(ack包),則會在 tcp_keepalive_intvl
後再次嘗試發送偵測包,直到收到對方的ack,如果一直沒有收到對方的ack,一共會嘗試 tcpkeepaliveprobes次,每次的間隔時間在這裏分別是15s, 30s, 45s, 60s, 75s。如果嘗試 tcp_keepalive_probes
次後,依然沒有收到對方的ack包,則會丟棄該TCP連接。TCP連接默認閒置時間是2小時,一般設置爲30分鐘足夠了。
管道化連接
在keep-alive的基礎上,我們可以做地更進一步,在響應到達之前,我們將多條請求按序放入請求隊列,服務端在收到請求後,必須按照順序對應請求的響應。但由於網絡環境非常複雜,因此即使請求是按順序發送的,也不一定是按順序到達服務端的。而且就算是服務端按序處理的,也不一定是按序返回給客戶端,所以最好是在響應中附帶一些可以標識請求的參數。
爲了安全起見,管道化的連接只適合“冪等”的請求,一般我們認爲:GET/HEAD/PUT/DELETE/TRACE/OPTIONS等方法都是冪等的。
小結
以上,就是所有HTTP的通信細節了,足夠在日常開發 作中使用了。更多沒有涉及的細節可以在用到的時候再去仔細研究