1. 程式人生 > >網路協議 12 - HTTP 協議:常用而不簡單

網路協議 12 - HTTP 協議:常用而不簡單

系列文章傳送門:

  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 協議,幾乎是每個人上網用的第一個協議,同時也是很容易被人忽略的協議。

    就像 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 存在的一些問題。

機制一:自定義連線機制

    我們知道,一條 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. 劉超 - 趣談網路協議系列課;