網絡協議 12 - HTTP 協議:經常使用而不簡單

系列文章傳送門:html

  1. 網絡協議 1 - 概述
  2. 網絡協議 2 - IP 是怎麼來,又是怎麼沒的?
  3. 網絡協議 3 - 從物理層到 MAC 層
  4. 網絡協議 4 - 交換機與 VLAN:辦公室太複雜,我要回學校
  5. 網絡協議 5 - ICMP 與 ping:投石問路的偵察兵
  6. 網絡協議 6 - 路由協議:敢問路在何方?
  7. 網絡協議 7 - UDP 協議:性善碰到城會玩
  8. 網絡協議 8 - TCP 協議(上):性惡就要套路深
  9. 網絡協議 9 - TCP協議(下):聰明反被聰明誤
  10. 網絡協議 10 - Socket 編程(上):實踐是檢驗真理的惟一標準
  11. 網絡協議 11 - Socket 編程(下):眼見爲實耳聽爲虛

    網絡協議五層通天路,我們從物理層、到鏈路層、網絡層、再到傳輸層,如今又進一步,來到了應用層。這也是咱們五層協議裏最上面的一層,關於應用層,有太多協議要了解。但要說最有名的,那確定就是 HTTP 了。編程

    HTTP 協議,幾乎是每一個人上網用的第一個協議,同時也是很容易被人忽略的協議。json

    就像 http://blog.muzixizao.com/,是個 URL,叫做統一資源定位符。之因此叫統一,是由於它是有規定格式的。HTTP 稱爲協議,blog.muzixizao.com 是一個域名,表示互聯網的一個位置。有的 URL 會有更詳細的位置標識,例如瀏覽器

http://blog.muzixizao.com/?p=140

    正是由於格式是統一的,因此當你把這樣一個字符串輸入到瀏覽器的框裏的時候,瀏覽器才知道如何進行統一處理。緩存

HTTP 請求的準備

    瀏覽器會將 blog.muzixizao.com 這個域名發送給 DNS 服務器,讓它解析爲 IP 地址。關於 DNS 解析的過程,較爲複雜,後面會專門介紹。安全

    域名解析成 IP 後,下一步是幹嗎呢?服務器

    還記得嗎?HTTP 是基於 TCP 協議的,因此接下來就是創建 TCP 鏈接了。具體的鏈接過程可點擊這裏查看網絡

    目前使用的 HTTP 協議大部分都是 1.1.在 1.1 協議裏面,默認開啓了 Keep-Alive 的,這樣創建的 TCP 鏈接,就能夠在屢次請求中複用。雖然 HTTP 用了各類方式來解決它存在的問題,但基於TCP 的它,每次創建鏈接的三次握手以及斷開鏈接的四次揮手,這個過程仍是挺費時的。若是好不容易創建了鏈接,而後作一點兒事情就結束了,未免太浪費了。架構

HTTP 請求的構建

    創建了鏈接之後,瀏覽器就要發送 HTTP 的請求。請求的格式以下圖:併發

    如圖,HTTP 的報文大概分爲請求行、首部、正文實體三部分。接下來,我們就來一一認識。

請求行

    在請求行中,URL 就是 http://blog.muzixizao.com,版本爲 HTTP 1.1。這裏要說一下的,就是對應的請求方法。有如下幾種類型:

1)GET 請求

    對於訪問網頁來說,最經常使用的類型就是 GET。顧名思義,GET 就是去服務器獲取一些資源。對於訪問網頁來說,要獲取的資源每每是一個頁面。其實也有不少其餘的格式,好比返回一個 JSON 字符串。固然,具體要返回什麼,是由服務端決定的。

    例如,在雲計算中,若是咱們的服務端要提供一個基於 HTTP 協議的 API,獲取全部雲主機的列表,就會使用 GET 方法請求,返回的多是一個 JSON 字符串,字符串裏面是一個列表,列表裏面會有各個雲主機的信息。

2)POST 請求
    另外一種類型叫作 POST。它須要主動告訴服務端一些信息,而非獲取。而要告訴服務端的信息,通常都放在正文裏面。正文裏有各類各樣的格式,最多見的的就是 JSON了。

    例如,咱們平時的支付場景,客戶端就須要把 「我是誰?我要支付多少?我要買什麼?」 這樣信息告訴服務器,這就須要 POST 方法。

    再如,在雲計算裏,若是咱們的服務器,要提供一個基於 HTTP 協議的建立雲主機的 API,也會用到 POST 方法。這個時候每每須要將 「我要建立多大的雲主機?多少 CPU 和多少內存?多大硬盤?」 這些信息放在 JSON 字符串裏面,經過 POST 的方法告訴服務器。

    除了上面常見的兩種類型,還有一種 PUT 類型,這種類型就是向指定資源位置上傳最新內容。可是 HTTP 的服務區每每是不容許上傳文件的,因此 PUT 和 POST 就都變成了要傳給服務器東西的方法。

    在咱們的實際使用過程當中,PUT 和 POST 仍是有區別的。POST 每每是用來建立一個資源,而 PUT 每每是用來更新一個資源。

    例如,雲主機已經建立好了,想對雲主機打一個標籤,說明這個雲主機是生產環境的,另一個雲主機是測試環境的。咱們修改標籤的請求每每就是用 PUT 方法。

    還有 DELETE 方法。這個是用來刪除資源的。

首部字段

    請求行下面就是首部字段。首部是 key-value 格式,經過冒號分割。這裏面,每每保存了一些很是重要的字段。

  • Accpet-Charset:客戶端能夠接受的字符集。防止傳過來的字符串客戶端不支持,從而出現亂碼;
  • Content-Type:正文格式。咱們進行 POST 請求時,若是正文是 JSON,咱們就應該將這個值設置爲 application/json;
  • 緩存字段 Cache-Control、If-Modified-Since。

    這裏重點認識下緩存字段。爲何要使用緩存呢?這是由於一個很是大的頁面有不少東西。

    例如,咱們瀏覽一個商品的詳情,裏面有商品的價格、庫存、展現圖片、使用手冊等待。

    商品的展現圖片會保持較長時間不變,而庫存胡一根筋用戶購買狀況常常改變。若是圖片很是大,而庫存數很是小,若是咱們每次要更新數據的時候都要刷新整個頁面,對於服務器的壓力也會很大。

    對於這種高併發場景下的系統,在真正的業務邏輯以前,都須要有個接入層,將這些靜態資源的請求攔在最外面。架構就像下圖:

    其中 DNS、CDN 會在後面的章節詳細說明。這裏我們就先來了解下 Nginx 這一層。它是若是處理 HTTP 協議呢?對於靜態資源,有 Vanish 緩存層,當緩存過時的時候,纔會訪問真正的 Tomcat 應用集羣。

    在 HTTP 頭裏面,Cache-Control 是用來控制緩存的。當客戶端發送的請求中包含 max-age 指令時,若是斷定緩存層中,資源的緩存時間數值比指定時間的數值校,那麼客戶端能夠接受緩存的資源;當指定 max-age 值爲 0,那麼緩存層一般須要將請求轉發給應用集羣。

    另外,If-Modified-Since 也是關於緩存的字段,這個字段是說,若是服務器的資源在某個時間以後更新了,那麼客戶端就應該下載最新的資源;若是沒有更新,服務端會返回「304 Not Modified」 的響應,那客戶端就不用下載了,也會節省帶寬。

    到此,咱們拼湊起了 HTTP 請求的報文格式,接下來,瀏覽器會把它交給傳輸層。

HTTP 請求的發送

    HTTP 協議是基於 TCP 協議的,因此它是以面向鏈接的方式發送請求,經過 stream 二進制流的方式傳給對方。固然,到了 TCP 層,它會把二進制流變成一個個的報文段發送給服務器。

    在發送給每一個報文段的時候,都須要對方有一個迴應 ACK,來保證報文可靠地到達了地方。若是沒有迴應,那麼 TCP 這一層會從新傳輸,直到能夠到達。同一個包有可能被傳了好屢次,可是 HTTP 這一層不須要知道這一點,由於是 TCP 這一層在埋頭苦幹。

然後續傳輸過程以下:

  1. TCP 層封裝目標地址和源地址。TCP 層發送每個報文的時候,都須要加上本身的地址和它想要去的地址,將這兩個信息放到 IP 頭裏面,交給 IP 層進行傳輸。
  2. IP 層獲取 MAC 頭。IP 層須要查看目標地址和本身是否在同一個局域網。若是是,就發送 ARP 協議來請求這個目標地址對應的 MAC 地址,而後將源 MAC 和目標 MAC 放入 MAC 頭,發送出去;若是不在同一個局域網,就須要發送到網關,這裏也要經過 ARP 協議來獲取網關的 MAC 地址,而後將源 MAC 和網關 MAC 放入 MAC 頭,發送出去。
  3. 網關轉發。網關收到包發現 MAC 符合,取出目標 IP 地址,根據路由協議找到下一跳的路由器,獲取下一跳路由器的 MAC 地址,將包發給下一跳路由器。
  4. 數據包到達目標地址的局域網。經過 ARP 協議獲取目標地址的 MAC 地址,將包發出去。
  5. 目標地址檢查信息,返回 ACK。目標機器發現數據包中的 MAC 地址及 IP 地址都和本機匹配,就根據 IP 頭中的協議類型,知道是 TCP 協議,解析 TCP 的頭,獲取序列號。判斷序列號是不是本機須要的,若是是,就放入緩存中而後返回一個 ACK,若是不是就丟棄。
  6. 根據端口號將數據包發送到指定應用。TCP 頭裏面還有端口號,HTTP 的服務器正在監聽這個端口號。因而,目標機器天然指定是 HTTP 服務器這個進程想要這個包,就把數據包發給 HTTP 服務器。
  7. HTTP 服務器處理請求。HTTP 服務器根據請求信息進行處理,並返回數據給客戶端。

HTTP 返回的構建

    HTTP 的返回報文也是有必定格式的,以下圖:

狀態行包含狀態碼和短語。狀態碼反應 HTTP 請求的結果。200 是大吉大利;404 則是咱們最不想見到的,也就是服務端沒法響應這個請求。短語中會說明出錯緣由。

首部 key-value。這裏經常使用的有如下字段:

  • Retry-After:客戶端應該在多長時間後再次嘗試鏈接;
  • Content-Type:返回數據格式

    構造好了返回的 HTTP 報文,接下來就是把這個報文發送出去。固然,仍是交給 Socket 去發送,交給 TCP,讓 TCP 返回的 HTML 分紅一個個小的數據段,而且保證每一段都安全到達。這些小的數據段會加上 TCP 頭,而後交給 IP 層,沿着來時的路反向走一遍。雖然不必定是徹底相同的路徑,可是邏輯過程是同樣的,一直到達客戶端。

    客戶端取出數據後 ,會根據端口號交給指定的程序,這時候就是咱們的瀏覽器出馬的時候。

    瀏覽器拿到了 HTTP 報文,發現返回 200,一切正常,就從正文中將 HTML 拿出來,展現出一個炫酷吊炸天的網頁。

    以上就是正常的 HTTP 請求與返回的完整過程。

HTTP 2.0

    上面提到了,如今用到 HTTP 大可能是 1.1 版本,而 HTTP 2.0 在 1.1 的基礎上進行了一些優化,以期解決一些問題。

    HTTP 1.1 在應用層以純文本的形式進行通訊。每次通訊都要帶完整的 HTTP 頭,並且不考慮 pipeline 模式的話,每次的過程都要像上面描述的那樣一去一回。顯然,在效率上會存在問題。

    爲了解決這些問題,HTTP 2.0 會對 HTTP 頭進行必定的壓縮,將原來每次都要攜帶的大量 key-value 對在兩端創建一個索引表,對相同的頭只發送索引表中的索引。

    另外,HTTP 2.0 協議將一個 TCP 鏈接切分紅多個流,每一個流都有本身的 ID,並且流能夠是客戶端發給服務端,也能夠是服務端發給客戶端,它其實只是個虛擬的通道,除此以外,它還有優先級。

    HTTP 2.0 將全部的傳輸信息分割成更小的消息和幀,並對它們採用二進制格式編碼。常見的幀有 Header 幀,用於傳輸 Header 內容,而且會開啓一個新的流。還有 Data 幀,用來傳輸正文實體,而且多個 Data 幀屬於同個流。

    經過這兩種機制,HTTP 2.0 的客戶端能夠將多個請求分到不一樣的流中, 而後將請求內容拆分紅幀,進行二進制傳輸。這些幀能夠打散亂序發送,而後根據幀首部的流標識符從新組裝,而且能夠根據優先級,決定先處理哪一個流的數據。

    針對 HTTP 2.0,咱們來看一個例子。

    假設咱們有一個頁面要發送三個獨立的請求,一個獲取 CSS、一個獲取 JS、一個獲取圖片 jsg。若是使用 HTTP 1.1,這三個請求就是串行的,可是若是使用 HTTP 2.0,就能夠在一個鏈接裏,客戶端和服務端同時反思多個請求和迴應,並且不用按照順序一對一對應。

    如上圖。HTTP 2.0 實際上是將三個請求變成三個流,將數據分紅幀,亂序發送到一個 TCP 鏈接中。

    HTTP 2.0 成功解決了 HTTP 1.1 的隊首阻塞問題。同時,也不須要經過 HTTP 1.x 的 pipeline 機制用多條 TCP 鏈接來實現並行請求與響應,減小了 TCP 鏈接數對服務器性能的影響,加快頁面組件的傳輸速度。

    HTTP 2.0 雖然大大增長了併發性,但因爲 TCP 協議的按序處理的特性,仍是會出現阻塞的問題。

    還記得我們以前說過的 QUIC 協議嗎?這時候就是它登場的時候了。

    它用如下四個機制,解決了 TCP 存在的一些問題。

QUIC 協議

機制一:自定義鏈接機制

    咱們知道,一條 TCP 鏈接是由四元組標識的。一旦一個元素髮生變化,就須要端口重連。這在移動互聯網的狀況下,當咱們切換網絡或者信號不穩定時,都會致使重連,從而增長時延。

    TCP 沒辦法解決上述問題,可是 QUCI 基於 UDP 協議,就能夠在本身的邏輯裏面維護鏈接的機制,再也不以四元組標識,而是以一個 64 位的隨機數做爲標識 ID,並且 UDP 是無鏈接的,只要 ID 不變,就不須要從新創建鏈接。

機制二:自定義重傳機制
    TCP 爲了保證可靠性,經過使用序號應答機制,來解決順序問題和丟包問題。

    任何一個序號的包發出去,都要在必定時間內獲得應答,不然就會超時重發。這個超時時間就是經過採樣往返時間 RTT 不斷調整的。其實,這個超時時間的採樣是不太準確的。

    如上圖。發送一個包,序號爲 100,超時後,再發送一個 100。而後收到了一個 ACK101。這個時候客戶端知道服務器已經收到了 100,可是往返時間怎麼計算呢?是 ACK 到達時間減去後一個 100 發送的時間,仍是減去前一個 100 發送的時間呢?前者把時間算短了,後者把時間算長了

    QUIC 也有一個序列號,是徹底遞增的。任何一個包發送一次後,下一次序列號就要加一。像咱們上面的例子,在 QUIC 協議中,100 的包沒有返回,再次發送時,序號就是 101 了,若是返回是 ACK100,就是對第一個包的響應,若是返回 ACK101,就是對第二個包的響應,RTT 時間計算相對準確,過程以下圖:

    上面的過程當中,有的童鞋可能會問了,兩個序號不同的包,服務器怎麼知道是一樣的內容呢?沒錯,這確實是個問題。爲了解決這個問題,QUIC 協議定義了一個 Offset 的概念。

    QUIC 既然是面向鏈接的,也就像 TCP 同樣,是一個數據流。,發送的數據在這個流裏面都有個偏移量 Offset,能夠經過 Offset 查看數據發送到了那裏,這樣只要這個 Offset 的包沒有來,就要重發。若是來了,就按照 Offset 拼接成一個流。

機制三:無阻塞的多路複用
    有了自定義的鏈接和重傳機制,咱們就能夠解決上面 HTTP 2.0 的多路複用問題。

    同 HTTP 2.0 同樣,同一條 QUIC 鏈接上能夠建立多個 stream,來發送多個 HTTP 請求。更棒的是,QUIC 是基於 UDP 的,一個鏈接上的多個 stream 之間沒有依賴。這樣,假如 stream2 丟了一個 UDP 包,後面跟着 stream3 的一個 UDP 包,雖然 stream2 的那個包須要重傳,可是 stream3 的包無需等待,就能夠發給用戶。

機制四:自定義流量控制
    TCP 的流量控制是經過滑動窗口協議。QUIC 的流量控制也是經過 window_update,來告訴對端它能夠接受的字節數。可是 QUIC 的窗口是適應本身的多路複用機制的,不但在一個鏈接上控制窗口,還在一個鏈接中的每一個 stream 控制窗口。

    還記得嗎?在 TCP 協議中,接收端的窗口的起始點是下一個要接收而且 ACK 的包,即使後來的包都到了,放在緩存裏面,窗口也不能右移,由於 TCP 的 ACK 機制是基於序列號的累計應答,一旦 ACK 一個序列號,就說明前面的都到了,因此只要前面的沒到,後面的即便到了也不能 ACK,就會致使後面的到了,也有可能超時重傳,浪費帶寬。

    QUIC 的 ACK 是基於 offset 的,每一個 offset 的包來了,進了緩存,就能夠應答,應答後就不會重發,中間的空檔會等待到來或者重發便可,而窗口的起始位置爲當前收到的最大 offset,從這個 offset 到當前的 stream 所能容納的最大緩存,是真正的窗口大小,顯然,這樣更加準確。

    另外,還有整個鏈接的窗口,須要對於全部的 stream 的窗口作一個統計。

小結

  • HTTP 協議雖然很經常使用,也很複雜,咱們只須要重點記住 GET、POST、PUT、DELETE 這幾個方法,以及重要的首部字段;
  • HTTP 2.0 經過頭壓縮、分幀、二進制編碼、多路複用等技術提高性能;
  • QUIC 協議經過基於 UDP 自定義的相似 TCP 的鏈接、重試、多路複用、流量控制技術,進一步提高性能。

參考:

  1. The TCP/IP Guide;
  2. 百度百科 - HTTP 詞條;
  3. 劉超 - 趣談網絡協議系列課;
相關文章
相關標籤/搜索