HTTP協議:看個新聞原來這麼麻煩

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

既然說看新聞,咱們就先登錄http://www.163.com

http://www.163.com是個URL,叫作統一資源定位符。之所以叫統一,是因爲它是有格式的。HTTP稱爲協議,www.163.com是一個域名,表示互聯網上的一個位置。有的URL會有更詳細的位置標識,例如http://www.163.com/index.html。正是因爲這個東西是統一的,所以當你把這樣一個字符串輸入到瀏覽器的框裏的時候,瀏覽器才知道如何進行統一處理。

HTTP請求的準備

**瀏覽器會將www.163.com這個域名發送給DNS服務器,讓它解析爲IP地址。**有關DNS的過程,其實非常複雜,這個在後面專門介紹DNS的時候,我會詳細描述,這裏我們先不管,反正它會被解析成爲IP地址。那接下來是發送HTTP請求嗎?

不是的,HTTP是基於TCP協議的,當然是要先建立TCP連接了,怎麼建立呢?還記得第11節講過的三次握手嗎?

目前使用的HTTP協議大部分都是1.1。在1.1的協議裏面,默認是開啓了Keep-Alive的,這樣建立的TCP連接,就可以在多次請求中複用。

學習了TCP之後,你應該知道,TCP的三次握手和四次揮手,還是挺費勁的。如果好不容易建立了連接,然後就做了一點兒事情就結束了,有點兒浪費人力和物力。

HTTP請求的構建

建立了連接以後,瀏覽器就要發送HTTP的請求,請求的格式就像這樣。

image

HTTP的報文大概分爲三大部分。第一部分是請求行,第二部分是請求的首部,第三部分纔是請求的正文實體

第一部分:請求行

在請求行中,URL就是http://www.163.com,版本爲HTTP 1.1。這裏要說一下的,就是方法。方法有幾種類型呢?

對於訪問網頁來講,最常用的類型就是GET。顧名思義,GET就是去服務器獲取一些資源。對於訪問網頁來講,要獲取的資源往往是一個頁面。其實也有很多其他的格式,比如說返回一個JSON字符串,到底要返回什麼,是由服務器端的實現決定的。

例如,在雲計算中,如果我們的服務器端要提供一個基於HTTP協議的API,獲取所有云主機的列表,這就會使用GET方法得到,返回的可能是一個JSON字符串。字符串裏面是一個列表,列表裏面是一項的雲主機的信息。

另外一種類型叫做POST。它需要主動告訴服務端一些信息,而非獲取。要告訴服務端什麼呢?一般會放在正文裏面。正文可以有各種各樣的格式。常見的格式也是JSON。

例如,我們下一節要講的支付場景,客戶端就需要把“我是誰?我要支付多少?我要買啥?”告訴服務器,這就需要通過POST方法。再如,在雲計算裏,如果我們的服務器端,要提供一個基於HTTP協議的創建雲主機的API,也會用到POST方法。這個時候往往需要將“我要創建多大的雲主機?多少CPU多少內存?多大硬盤?”這些信息放在JSON字符串裏面,通過POST的方法告訴服務器端。

還有一種類型叫PUT,就是向指定資源位置上傳最新內容。但是,HTTP的服務器往往是不允許上傳文件的,所以PUT和POST就都變成了要傳給服務器東西的方法。

在實際使用過程中,這兩者還會有稍許的區別。POST往往是用來創建一個資源的,而PUT往往是用來修改一個資源的。例如,雲主機已經創建好了,我想對這個雲主機打一個標籤,說明這個雲主機是生產環境的,另外一個雲主機是測試環境的。那怎麼修改這個標籤呢?往往就是用PUT方法。

再有一種常見的就是DELETE。這個顧名思義就是用來刪除資源的。例如,我們要刪除一個雲主機,就會調用DELETE方法。

第二部分:首部字段

請求行下面就是我們的首部字段。首部是key value,通過冒號分隔。這裏面,往往保存了一些非常重要的字段。

例如,Accept-Charset,表示客戶端可以接受的字符集,防止傳過來的是另外的字符集,從而導致出現亂碼。再如,Content-Type是指正文的格式。例如,我們進行POST的請求,如果正文是JSON,那麼我們就應該將這個值設置爲JSON。

這裏需要重點說一下的就是緩存。爲啥要使用緩存呢?那是因爲一個非常大的頁面有很多東西。例如,我瀏覽一個商品的詳情,裏面有這個商品的價格、庫存、展示圖片、使用手冊等等。商品的展示圖片會保持較長時間不變,而庫存會根據用戶購買的情況經常改變。如果圖片非常大,而庫存數非常小,如果我們每次要更新數據的時候都要刷新整個頁面,對於服務器的壓力就會很大。

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

image

其中DNS、CDN我在後面的章節會講。和這一節關係比較大的就是Nginx這一層,它如何處理HTTP協議呢?對於靜態資源,有Vanish緩存層。當緩存過期的時候,纔會訪問真正的Tomcat應用集羣。

在HTTP頭裏面,Cache-control是用來控制緩存的。當客戶端發送的請求中包含max-age指令時,如果判定緩存層中,資源的緩存時間數值比指定時間的數值小,那麼客戶端可以接受緩存的資源;當指定max-age值爲0,那麼緩存層通常需要將請求轉發給應用集羣。

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

目前爲止,我們僅僅是拼湊起來了HTTP請求的報文格式,接下來,瀏覽器會把它交給下一層傳輸層。怎麼交給傳輸層呢?其實也無非是用Socket這些東西,只不過用的瀏覽器裏,這些程序不需要你自己寫,有人已經幫你寫好了。

HTTP請求的發送

HTTP協議是基於TCP協議的,所以它使用面向連接的方式發送請求,通過stream二進制流的方式傳給對方。當然,到了TCP層,它會把二進制流變成一個的報文段發送給服務器。在發送給每個報文段的時候,都需要對方有一個迴應ACK,來保證報文可靠地到達了對方。如果沒有迴應,那麼TCP這一層會進行重新傳輸,直到可以到達。同一個包有可能被傳了好多次,但是HTTP這一層不需要知道這一點,因爲是TCP這一層在埋頭苦幹。

TCP層發送每一個報文的時候,都需要加上自己的地址(即源地址)和它想要去的地方(即目標地址),將這兩個信息放到IP頭裏面,交給IP層進行傳輸。

IP層需要查看目標地址和自己是否是在同一個局域網。如果是,就發送ARP協議來請求這個目標地址對應的MAC地址,然後將源MAC和目標MAC放入MAC頭,發送出去即可。如果不在同一個局域網,就需要發送到網關,還要需要發送ARP協議,來獲取網關的MAC地址,然後將源MAC和網關MAC放入MAC頭,發送出去。

網關收到包發現MAC符合,取出目標IP地址,根據路由協議找到下一跳的路由器,獲取下一跳路由器的MAC地址,將包發給下一跳路由器。這樣路由器一跳一跳終於到達目標的局域網。這個時候,最後一跳的路由器能夠發現,目標地址就在自己的某一個出口的局域網上。於是,在這個局域網上發送ARP,獲得這個目標地址的MAC地址,將包發出去。

目標的機器發現MAC地址符合,就將包收起來;發現IP地址符合,根據IP頭中協議項,知道自己上一層是TCP協議,於是解析TCP的頭,裏面有序列號,需要看一看這個序列包是不是我要的,如果是就放入緩存中然後返回一個ACK,如果不是就丟棄。

TCP頭裏面還有端口號,HTTP的服務器正在監聽這個端口號。於是,目標機器自然知道是HTTP服務器這個進程想要這個包,於是將包發給HTTP服務器。HTTP服務器的進程看到,原來這個請求是要訪問一個網頁,於是就把這個網頁發給客戶端。

HTTP返回的構建

HTTP的返回報文也是有一定格式的。這也是基於HTTP 1.1的。

image

狀態碼會反應HTTP請求的結果。“200”意味着大吉大利;而我們最不想見的,就是“404”,也就是“服務端無法響應這個請求”。然後,短語會大概說一下原因。

接下來是返回首部的key value。

這裏面,Retry-After表示,告訴客戶端應該在多長時間以後再次嘗試一下。“503錯誤”是說“服務暫時不再和這個值配合使用”。

在返回的頭部裏面也會有Content-Type,表示返回的是HTML,還是JSON。

構造好了返回的HTTP報文,接下來就是把這個報文發送出去。還是交給Socket去發送,還是交給TCP層,讓TCP層將返回的HTML,也分成一個個小的段,並且保證每個段都可靠到達。這些段加上TCP頭後會交給IP層,然後把剛纔的發送過程反向走一遍。雖然兩次不一定走相同的路徑,但是邏輯過程是一樣的,一直到達客戶端。

客戶端發現MAC地址符合、IP地址符合,於是就會交給TCP層。根據序列號看是不是自己要的報文段,如果是,則會根據TCP頭中的端口號,發給相應的進程。這個進程就是瀏覽器,瀏覽器作爲客戶端也在監聽某個端口。

當瀏覽器拿到了HTTP的報文。發現返回“200”,一切正常,於是就從正文中將HTML拿出來。HTML是一個標準的網頁格式。瀏覽器只要根據這個格式,展示出一個絢麗多彩的網頁。

這就是一個正常的HTTP請求和返回的完整過程。

HTTP 2.0

當然HTTP協議也在不斷地進化過程中,在HTTP1.1基礎上便有了HTTP 2.0。

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的客戶端可以將多個請求分到不同的流中,然後將請求內容拆成幀,進行二進制傳輸。這些幀可以打散亂序發送, 然後根據每個幀首部的流標識符重新組裝,並且可以根據優先級,決定優先處理哪個流的數據。

我們來舉一個例子。假設我們的一個頁面要發送三個獨立的請求,一個獲取css,一個獲取js,一個獲取圖片jpg。如果使用HTTP 1.1就是串行的,但是如果使用HTTP 2.0,就可以在一個連接裏,客戶端和服務端都可以同時發送多個請求或迴應,而且不用按照順序一對一對應。

image

HTTP 2.0其實是將三個請求變成三個流,將數據分成幀,亂序發送到一個TCP連接中。

image

HTTP 2.0成功解決了HTTP 1.1的隊首阻塞問題,同時,也不需要通過HTTP 1.x的pipeline機制用多條TCP連接來實現並行請求與響應,減少了TCP連接數對服務器性能的影響,同時將頁面的多個數據css、js、 jpg等通過一個數據鏈接進行傳輸,能夠加快頁面組件的傳輸速度。

QUIC協議的“城會玩”

HTTP 2.0雖然大大增加了併發性,但還是有問題的。因爲HTTP 2.0也是基於TCP協議的,TCP協議在處理包時是有嚴格順序的。當其中一個數據包遇到問題,TCP連接需要等待這個包完成重傳之後才能繼續進行。雖然HTTP 2.0通過多個stream,使得邏輯上一個TCP連接上的並行內容,進行多路數據的傳輸,然而這中間並沒有關聯的數據。一前一後,前面stream 2的幀沒有收到,後面stream 1的幀也會因此阻塞。

於是,就又到了從TCP切換到UDP,進行“城會玩”的時候了。這就是Google的QUIC協議,接下來我們來看它是如何“城會玩”的。

機制一:自定義連接機制

我們都知道,一條TCP連接是由四元組標識的,分別是源 IP、源端口、目的 IP、目的端口。一旦一個元素髮生變化時,就需要斷開重連,重新連接。在移動互聯情況下,當手機信號不穩定或者在WIFI和 移動網絡切換時,都會導致重連,從而進行再次的三次握手,導致一定的時延。

這在TCP是沒有辦法的,但是基於UDP,就可以在QUIC自己的邏輯裏面維護連接的機制,不再以四元組標識,而是以一個64位的隨機數作爲ID來標識,而且UDP是無連接的,所以當IP或者端口變化的時候,只要ID不變,就不需要重新建立連接。

機制二:自定義重傳機制

前面我們講過,TCP爲了保證可靠性,通過使用序號應答機制,來解決順序問題和丟包問題。

任何一個序號的包發過去,都要在一定的時間內得到應答,否則一旦超時,就會重發這個序號的包。那怎麼樣纔算超時呢?還記得我們提過的自適應重傳算法嗎?這個超時是通過採樣往返時間RTT不斷調整的。

其實,在TCP裏面超時的採樣存在不準確的問題。例如,發送一個包,序號爲100,發現沒有返回,於是再發送一個100,過一陣返回一個ACK101,這個時候客戶端知道這個包肯定收到了,但是往返時間是多少呢?是ACK到達的時間減去後一個100發送的時間,還是減去前一個100發送的時間呢?第一種算法把時間算短了,第二種算法把時間算長了。

QUIC也有個序列號,是遞增的。任何一個序列號的包只發送一次,下次就要加一了。例如,發送一個包,序號是100,發現沒有返回;再次發送的時候,序號就是101了;如果返回的ACK 100,就是對第一個包的響應。如果返回ACK 101就是對第二個包的響應,RTT計算相對準確。

但是這裏有一個問題,就是怎麼知道包100和包101發送的是同樣的內容呢?QUIC定義了一個offset概念。QUIC既然是面向連接的,也就像TCP一樣,是一個數據流,發送的數據在這個數據流裏面有個偏移量offset,可以通過offset查看數據發送到了哪裏,這樣只要這個offset的包沒有來,就要重發;如果來了,按照offset拼接,還是能夠拼成一個流。

image

機制三:無阻塞的多路複用

有了自定義的連接和重傳機制,我們就可以解決上面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所能容納的最大緩存,是真正的窗口大小。顯然,這樣更加準確。

image

另外,還有整個連接的窗口,需要對於所有的stream的窗口做一個統計。

好了,今天就講到這裏,我們來總結一下:

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

本文出自極客時間的《趣談網絡協議》專欄,目前超過3w名程序員加入學習,極客時間最受歡迎課程之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章