完整的一次 HTTP 請求響應過程(一)

YangAM發表於2018-06-01

因特網無疑是人類有史以來最偉大的設計,它互聯了全球數億臺計算機、通訊裝置,即便位於地球兩端的使用者也可在頃刻間完成通訊。

可以說『協議』是支撐這麼一個龐大而複雜的系統有條不紊運作的核心,而所謂『協議』就是通訊雙方所必須遵守的規則,在這種規則下,不同的資料包可能被解析為不同的響應動作。

簡而言之,『協議』就是指如果傳送和接收方按照這個規則進行資料包文的傳送,即可在基本的資料傳輸之上得到某些特殊的功能或服務,否則你的資料別人是不認識的。例如:遵循 TCP 協議的兩端,可以在不可靠的網路傳輸中得到可靠的資料傳輸能力。

整個計算機網路是分層的,有七層模型,也有五層模型,個人覺得五層模型更利於理解。我們從上至下的介紹這五個層,它們分別是,應用層,運輸層,網路層,資料鏈路層和物理層

應用層

『應用層』算是距離使用者最近的一層了,主機上的一個個的程式就構成了『應用層』。比如你在你的瀏覽器位址列輸入了 「www.baidu.com」,你的瀏覽器在應用層會做哪些事情呢?

首先瀏覽器會使用 DNS 協議返回域名「www.baidu.com」所對應的 IP 地址,關於 DNS 我們待會詳細介紹。

接著,應用層決定建立一個『TCP 套接字』,然後將這個請求動作封裝成一個 Http 資料包並推入套接字中。

套接字分為兩種型別,『TCP 套接字』和『UDP 套接字』,應用層同時可能會有幾十個資料包的發出,而運輸層也會收到所有的響應報文,那麼它該如何區分這些報文到底是誰的響應報文呢?

而套接字就是用於區分各個應用層應用的,往往由埠號和 IP 地址進行標識,運輸層只要檢視響應報文的源埠號和 IP 地址就能夠知道該將報文推送給哪個套接字了。

當一個應用層資料包被推動進套接字之後,應用層的所有工作也算是全部完成了,關於後續報文的去向,它已經不用管了。

這裡還要說明一點的是,『TCP 套接字』和『UDP 套接字』兩者本質上的區別在於,前者保證資料包可靠地到達目的地,但是必然耗時,而後者不保證資料包一定能到達目的地,但是速度快,這也是應用層協議在選擇運輸層協議的時候需要考慮的一點。

關於 TCP 和 UDP,我們後續還會繼續說,下面我們看看域名解析協議 DNS 是如何運作的,它是如何將一個域名解析返回它的 IP 地址的。

DNS 原理

首先明確一點的是,DNS 是一個應用層協議,並且它選擇的運輸層協議是 UDP,所以你的域名解析過程一般會很快,但也會經常出現解析失敗的情況,然而重新整理一下又好了。

image

在 DNS 伺服器上,域名和它所對應的 IP 地址儲存為一條記錄,而所有的記錄都不可能只儲存在一臺伺服器上,我相信無論多麼強大的伺服器都扛不住全球上億次的併發量吧。

大致來說,有三種型別的 DNS 伺服器,根 DNS 伺服器,頂級域 DNS 伺服器和權威 DNS 伺服器。

其中,頂級域 DNS 伺服器主要負責諸如 com、org、net、edu、gov 等頂級域名。

根 DNS 伺服器儲存了所有頂級域 DNS 伺服器的 IP 地址,也就是說你可以通過根伺服器找到頂級域伺服器。例如:「www.baidu.com」,根伺服器會返回所有維護 com 這個頂級域伺服器的 IP 地址。

然後你任意選擇其中一個頂級域伺服器,請求該頂級域伺服器,該頂級域伺服器拿到域名後應當能夠做出判斷並給出負責當前域的權威伺服器地址,以百度為例的話,頂級域伺服器將返回所有負責 baidu 這個域的權威伺服器地址。

於是你可以任意選擇其中一個權威伺服器地址,向它繼續查詢 「www.baidu.com」 的具體 IP 地址,最終權威伺服器會返回給你具體的 IP 地址。

至此,我們簡單描述了一個域名解析的大致過程,還有一些細節之處並未提及,我們等會會通過一個例項來完整的看一下,下面描述一個非常重要的概念。

整個 DNS 解析過程中,有一個非常核心的人物我們一直沒介紹它,它就像主機的『助理』一樣,幫助主機查詢域名的 IP 地址。它叫做『本地 DNS 伺服器』。

image

大家每次通過 DHCP 動態獲取 IP 地址的時候,這一點後文會說。其實路由器不僅給你返回了 IP 地址,還會告訴你一個 DNS 伺服器地址,這個就是你的本地 DNS 伺服器地址,也就是說,你的所有域名解析請求只要告訴它就行了,它會幫你查並返回結果給你的。

除此之外,本地 DNS 伺服器往往是具有快取功能的,通常兩天內的記錄都會被快取,所以大部分時候你是感覺不到域名解析過程的,因為往往就是從快取裡拿的,非常快。

下面我們看一個簡單的案例:

網上找的一個圖,自己畫實在太費時間了,但足以說明問題,現在假設請求 「www.xx.com」 。

image

  • ①:主機向負責自己的本地 DNS 傳送查詢報文,如果本地伺服器快取中有,將直接返回結果
  • ②:本地伺服器發現快取中沒有,於是從內建在內部的根伺服器列表中選一個傳送查詢報文
  • ③:根伺服器解析一下字尾名,告訴本地伺服器負責 .com 的所有頂級伺服器列表
  • ④:本地伺服器選擇一個頂級域伺服器繼續查詢,.com 域伺服器拿到域名後繼續解析,返回負責 .xx 域的所有權威伺服器列表
  • ⑥:本地伺服器從返回的權威伺服器之一再次傳送查詢報文,最終會從某一個權威伺服器上得到具體的 IP 地址
  • ⑧:向主機返回結果

其實整個 DNS 報文的傳送與響應過程都是要走我們的五層協議的,只是這裡重點在於理解 DNS 協議本身,所以並未提及其他層的具體細節,這裡的強調是提醒你 DNS 只是一個應用層協議。

運輸層

運輸層的任務就是將應用層推出套接字的所有資料包收集起來,並且按照應用層指定的運輸層協議,TCP 或 UDP,重新封裝應用層資料包,並推給網路層等待傳送。

TCP 和 UDP 是運輸層的兩個協議,前者是基於連線的可靠傳輸協議,後者是無連線的不可靠傳輸協議,所以前者更適合於一些對資料完整性要求高的場合,後者則適合於那種可以允許資料丟失但對傳輸速率要求特別高的場景,例如:語音電話,視訊等,丟一兩個包最多卡頓一下,無傷大雅。

UDP

UDP 不同於 TCP 那樣複雜,它既不保證資料可靠的傳輸到目的地,也不保證資料按序到達目的地,僅僅提供了簡單的差錯檢驗。報文格式如下:

image

其中,資料就是應用層推出來的資料,源埠號用於響應報文的交付,目的埠號用於向目的程式交付資料,校驗和用於檢查傳輸過程中資料是否受損,如果受損,UDP 將直接丟棄該報文。

TCP

TCP 要稍微複雜些,它是面向連線的,並且基於連線提供了可靠的資料傳輸服務,它的資料包文格式如下:

image

單純的解釋報文格式中各個欄位的含義並沒有太過實際的意義,你也很難理解了,在我們介紹 TCP 是如何『三次握手』,『四次揮手』以及『丟包重傳』等動作時,不間斷的會說明這些動作時如何使用報文中的相關欄位的。

首先我們來看耳熟能詳的『三次握手』,這基本上是 TCP 的代名詞了,無論懂不懂具體原理的人,提到 TCP,基本上都是知道『三次握手』的。

而本身,TCP 的三次握手就是為了確保通訊雙方能夠穩定的建立連線並完成資料包文的請求與響應動作,至於為什麼是三次握手而不是四次五次,這是一個哲學問題,這裡就不做討論了。

第一步:

客戶端向服務端傳送一份特殊的 TCP 報文,該報文並不包含應用層的資料,是一份特殊的報文,它的 TCP 首部中 SYN 欄位值為 1 (參見上述報文格式)。

除此之外,客戶端還會隨機生成一個初始序號,填在報文的「序號」欄位,代表當前報文的序號是這個,並且我後續的分組會基於這個序號遞增。

然後該報文將會經網路層、鏈路層、物理層傳送到服務端。

第二步:

如果分組丟失了,那麼客戶端會經過某個時間間隔再次嘗試傳送。

而如果分組準確的到達服務端了,服務端拆開 TCP 首部會看到,這是一個特殊的 SYN 握手報文,於是為此次連線分配快取等資源。

接著服務端開始構建響應報文,SYN 是一個用於同步需要的欄位,響應報文中依然會被置為 1,並且服務端也將隨機生成一個初始序號放置的響應報文的序號欄位中。

最後,服務端還會為響應報文中的確認欄位賦值,這個值就是客戶端發過來的那個序號值加一。

整體上的意思就是說,「我同意你的連線請求,我的初始序號為 xxx,你的初始序號我收到了,我等著你的下一個分組到來」

第三步:

客戶端收到服務端的響應報文,於是分配客戶端 TCP 連線所必須的快取等資源,於是連線已經建立。

實際上從第三步開始,客戶端就可以攜帶應用層資料向服務端交換報文了,以後的每份報文中,SYN 都為 0,因為它只是用於同步初始序號的,這一點需要明確。

總的來說,整個『握手』過程大致如下圖所示:

image

下面我們看看拆除一條 TCP 連線的『四次揮手』是怎樣的過程。

因為一條 TCP 連線會消耗大量的主機資源,不僅僅服務端需要分配各種快取資源,客戶端也同樣需要分配相應資源。因為 TCP 是『全雙工通訊』,服務端和客戶端兩方其實是一樣的,誰是客戶誰是伺服器是相對的。

強調這一點是為了說明,一條 TCP 連線不是隻有客戶端才能斷開,服務端也同樣可以主動斷開連線,這一點需要清楚。

我們這裡假設客戶端主動發起斷開連線的請求為例:

第一步:

客戶端構建一份特殊的 TCP 報文,該報文首部欄位 FIN 被置為 1,然後傳送該報文。

第二步:

服務端收到該特殊的 FIN 報文,於是響應客戶端一個 ACK 報文,告訴客戶端,請求關閉的報文已經收到,我正在處理。

第三步:

服務端傳送一個 FIN 報文,告訴客戶端,我將要關閉連線了。

第四步:

客戶端返回一個 ACK 響應報文,告訴服務端,我收到你剛才發的報文了,我已經確認,你可以關閉連線了。

當服務端收到客戶端傳送的 ACK 響應報文時,將釋放服務端用於該 TCP 連線的所有資源,與此同時,客戶端也會定時等待一定時間後完全釋放自己用於該連線的相關資源。

用一張圖更直觀的描述一下:

image

結合著圖與相關序號資訊,我們再詳細說說其中的一些細節。

首先,客戶端傳送一個特殊分組,該分組的序號為 u。傳送完成之後,客戶端進入 FIN-WAIT-1 這個狀態,這個狀態下,該 TCP 連線的客戶端不再能傳送資料包,但是是可以接受資料包的,它等待著服務端的響應報文。

接著,服務端收到客戶端傳送的終止連線報文請求,服務端構建響應報文,告訴客戶端「序號 u+1 以前的分組我都收到了」,並且進入 CLOSE-WAIT 狀態,這個狀態持續時間很短。

服務端會緊接著傳送它的 FIN 資料包,通知客戶端我服務端即將關閉連線,並隨即進入 LAST_ACK 狀態等待客戶端響應報文。

一旦客戶端收到這個 FIN 報文,將返回確認報文並進入 TIME-WAIT 狀態,等待 2MSL 時間間隔後完全釋放客戶端 TCP 連線所佔用資源。

與此同時,當服務端收到客戶端最後的確認報文,就將直接斷開服務端連線並釋放相關資源。

至於為什麼最後客戶端需要等 2MSL 時間長度再完全釋放 TCP 相關資源呢?

那是因為 2MSL 是一份報文存在於網路中最長的時間,超過該時間到達的報文都將被丟棄,而如果客戶端最後的確認報文於網路中丟失的話,服務端必將發起超時請求,重新傳送第三次揮手動作,此時等待中的客戶端就可隨即重新傳送一份確認請求。

這是為什麼客戶端等待一個最長報文傳輸時間的原因。有人可能好奇為什麼前面的各次請求都沒有做超時等待而只最後一次資料傳送做了超時等待?

其實原因很簡單,相信你也能想到,就是 TCP 自帶計時能力,超過一定時間沒有收到某個報文的確認報文,會自動重新傳送,而這裡如果不做等待而直接關閉連線,那麼我如何知道服務端到底收到沒我的確認報文呢。

通過等待一個最長週期,如果這個週期內沒有收到服務端的報文請求,那麼我們的確認報文必然是到達了服務端了的,否則重複傳送一次即可。

至此,TCP 的『三次握手』和『四次揮手』我們已經簡單描述完成了,下面我們看看 TCP 的一些其他特性,比如:可靠傳輸,擁塞控制等

首先我們來看 TCP 是如何實現可靠傳輸的,即如何解決網路傳輸中丟包的問題。

TCP 使用『回退 N 步』協議實現的可靠傳輸,準確來說,TCP 是在它的基礎上進行了一部分優化。

image

『回退 N 步』協議也被稱作『滑動視窗』協議,即最多允許傳送方有 N 個「已傳送但未被確認」的資料包文,如圖所示,p1 到 p3 長度即為 N,這裡的視窗指的就是 p1 到 p3 這個區間。

只有當傳送端收到 p1 的確認報文後,整個視窗才能向前滑動,而實際上在沒有收到 p1 的確認報文前,即便它後面的報文已經被接收,服務端也僅僅會快取這些『非預期的報文』

直到服務端收到最小預期的那個報文後,從快取中取出已經到達的後續報文,合併並向上交付,然後向傳送端返回一個確認報文。

當傳送端視窗從左往右已經連續多個報文被確認後,整個視窗將向前滑動多個單位長度。

下面我們看一個例子:

image

這是一個傳送方的視窗,灰色表示已經被確認的報文,黃色表示已傳送但未被確認的報文,綠色表示下一個待傳送的報文,白色表示不可用的報文。

這是我們假設服務端已經收到 6、7 兩份報文,但是它上一次向上交付給應用層的是 4 號報文,也就是說它在等 5 號報文,所以它暫時會將 6、7 兩個報文快取起來,等到 5 號報文來了一併交付給應用層。

現在 5 號報文由於超時被重傳了,終於到達目的地了,如願以償,服務端向上交付 5、6、7 三份報文,並返回一份確認報文,ACK = 8,表示序號 8 以前的所有報文都收到了

當傳送端收到這份確認報文後,5、6、7 變成灰色,視窗向前移動三個單位長度。

此外,我還想強調一個細節,TCP 是沒有否定確認的,所以如果服務端連續響應的多份報文是對同一序號的確認,那很有可能該序號以後的某個報文丟失。

例如:如果服務端傳送多個對分組 5 的 ACK 確認,那說明什麼?說明目前我服務端完整的向上交付的序號是 5 號,後續的報文我沒收到,你最好重新發一下別等待超時了。

這也是『快速重傳』的核心原理。

那麼 TCP 的可靠傳輸我們也基本介紹完了,下面我們看看如果網路擁塞的時候,TCP 是如何控制傳送流量的呢?

TCP 認為:丟包即擁塞,需要降低傳送效率,而每一次收到確認資料包即認為網路通暢,會增加傳送效率。

TCP 的擁塞控制演算法包含三個部分,慢啟動、擁塞避免和快速恢復

慢啟動的思想是,剛開始緩慢的傳送,比如某個時間段內只傳送一次資料包,當收到確認報文後,下一次同樣的時間間隔內,將傳送兩倍速率的兩份資料包,並以此類推。

所以,短時間內,一個 TCP 連線的傳送方將以指數級增長,但一旦出現丟包,即收到冗餘的 ACK 確認,或者對於一個包的確認 ACK 始終沒收到而不得不啟動一次超時重傳,那麼傳送方認為「網路是擁塞的」。

於是將速率直接調成一,即一個往返時間段,只傳送一個分組,並且設定一個變數 ssthresh 表述一個閾值的概念,這個值是上次丟包時傳送方傳送速率的一半。

之後的傳送方的傳送效率一樣會以指數級增長,但是不同於第一次,這次一旦達到這個閾值,TCP 將進入『擁塞避免』模式,該模式下的傳送效率將不再指數級增長,會謹慎的增長。

擁塞避免的思想是,每個往返時間段傳送的所有資料包全部得到確認後,下一次就增加一個分組的傳送,這樣緩慢的增長效率是謹慎的。

那麼一旦出現傳送端超時丟包,注意這裡是超時,將傳送速率置為一併重新進入慢啟動狀態,閾值就是當前傳送效率的一半。

而如果是服務端返回多個冗餘 ACK 以明確你丟包,TCP 認為這不是嚴重的,對於這種情況,TCP 減半當前傳送效率並進入快速恢復階段。

快速恢復的基本思想是,收到幾個冗餘的 ACK 就增加幾個分組的傳送效率,就是說,你服務端不是沒收到我的幾個報文嗎,這兩次傳送我提升速率迅速發給你。

當這期間出現了由傳送端超時導致的丟包,同樣的處理方式,初始化傳送速率為一併減半當前傳送效率作為閾值,進入慢啟動階段。

當然,如果這期間收到了對丟失報文的確認,那麼將適當降低傳送效率並進入擁塞避免狀態。

這樣,整個 TCP 最核心的幾個思想都已經介紹完了,整個運輸層基本上也算明瞭了。關於運輸層,你應當有了一定的理解,我再總結一下。

運輸層的任務就是從應用層的各個程式的套接字那取回來所有需要傳送的資料,然後選擇 TCP 或者 UDP 將資料封裝並推給下面的網路層待傳送。

未完,待續。。。


文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:撲在程式碼上的高爾基,所有文章都將同步在公眾號上。

image

相關文章