詳解網絡鏈接

一直以來,對於網絡鏈接中的細節都不是很清楚,最近特地梳理了一下,大部份內容來自書籍網絡是怎樣鏈接的(戶根勒)html

首先看下鏈接的總體流程nginx

1.輸入URL

在瀏覽器中輸入URL網址就能夠獲得咱們想要的網頁,這有兩個要素,瀏覽器和URL網址。瀏覽器是一個具有多種客戶端功能的綜合性客戶端軟件,它須要一些東西來判斷應該使用其中哪一種功能來訪問相應的數據, 而各類不一樣的URL 就是用來幹這個的, 好比訪問 Web 服務器時用「http:」, 而訪問 FTP服務器時用「 ftp:」。有了url瀏覽器就會經過咱們url定義的規則去訪問咱們須要的資源。git

下圖是幾種URL的含義web

2.DNS域名解析

咱們知道域名背後指向的是服務器地址,顯然這種關係不是憑空產生的,須要咱們本身來定義,因此就有了域名解析。經過域名解析能夠把域名指向對應的服務器地址,構成映射關係,這樣別人就能夠經過域名找到你的主機了。若是你有購買過域名,服務商確定會提供域名解析管理頁面,相似下面,這樣你就能夠根據本身的狀況,定義想要的映射關係。apache

2.1 爲何要用域名和ip地址的組合呢?

域名是給人來用的,爲了便於記憶,人們要記住一串由數字組成的ip地址是十分困難的。ip地址是給機械使用的,在信息傳遞過程當中,存在無數的路由器, 它們之間相互配合, 根據 IP地址來判斷應該把數據傳送到什麼地方。IP 地址的長度爲 32 比特, 也就是 4 字節,相對地, 域名最短也要幾十個字節, 最長甚至能夠達到 255 字節。 換句話說, 使用 IP 地址只須要處理 4 字節的數字,而域名則須要處理幾十個到 255個字節的字符,這增長了路由器的負擔,傳送數據也會花費更長的時間。因此須要有一個機制可以經過名稱來查詢 IP 地址, 或者經過 IP地址來查詢名稱,這樣就可以在人和機器雙方都不作出犧牲的前提下完美地解決問題。這個機制就是 DNS。瀏覽器

2.2 DNS域名解析過程

DNS 中的域名都是用句點來分隔的, 好比 www.lab.glasscom.com, 這裏的句點表明了不一樣層次之間的界限。 在域名中, 越靠右的位置表示其層級越高, 好比 www.lab.glasscom.com 這個域名若是按照公司裏的組織結構來講, 大概就是「 com 事業集團 glasscom 部 lab 科的 www」這樣。 其中, 至關於一個層級的部分稱爲域。 所以, com 域的下一層是glasscom 域, 再下一層是 lab 域, 再下面纔是 www 這個名字。真是由於這種層次的關係,上級保存下級的信息,因此咱們只需找到最上層的信息就能夠找到全部的信息了。com,cn,jp這樣的算得上是頂層DNS服務器,但實際上上面還有一層就是根域,根域的 DNS 服務器信息保存在互聯網中全部的 DNS 服務器中。緩存

下圖表示的就是DNS的查詢過程,先找到最近的DNS服務器(本地DNS服務器,也就是客戶端TCP/IP設置中填寫的DNS服務器地址),而後再去訪問根域DNS服務器,而後一層一層往下找。服務器

在實際的狀況更復雜,爲了加快查找,有些層會有緩存,這樣就不必定要從根域名開始往下找,大致流程以下。網絡

1> 首先,檢查瀏覽器是否有緩存

瀏覽器自己是會緩存域名的,可是這是有限制的,不只瀏覽器緩存大小有限制,並且緩存的時間也有限制。這個緩存時間太長和過短都很差,若是緩存時間太長,一旦域名被解析到的IP有變化,會致使被客戶端緩存的域名沒法解析到變化後的IP地址,以至該域名不能正常解析。

2> 其次,檢查系統緩存 + hosts

操做系統自身也會有dns緩存

咱們能夠經過設置hosts文件內容來進行域名綁定

3> 若是以上都沒有命中,那麼就會檢查本地DNS服務器(LDNS)

LDNS通常是電信運營商提供的,也可使用像Google提供的DNS服務器。

在咱們的網絡配置中都會有「DNS服務器地址」這一項,這個地址就用於解決前面所說的若是兩個過程沒法解析時要怎麼辦,操做系統會把這個域名發送給這裏設置的LDNS,也就是本地區的域名服務器。這個DNS一般都提供給你本地互聯網接入的一個DNS解析服務。大約80%的域名解析都到這裏就已經完成了,因此LDNS主要承擔了域名的解析工做。

4> 依然沒有命中,則直接查找最頂端的根域DNS服務器

根域DNS服務器會返回頂層DNS服務器地址給本地DNS服務器。

5> 接下來,訪問 4 步中返回的頂層DNS服務器

本地DNS服務器拿到DNS頂層服務器地址後會去發送請求,頂層DNS服務器查詢後返回管理方DNS服務器地址給本地DNS服務器。

6> 最後,訪問 5 步中返回的管理方DNS服務器

本地DNS服務器拿到管理方DNS服務器地址後會去發送請求,管理方DNS服務器查詢存儲的域名和IP的映射關係表,並返回給本地DNS服務器,本地DNS服務器返回該域名對應的IP和TTL值給瀏覽器。

2.3 DNS查詢IP的底層原理

下面是原理的僞代碼,能夠按照這個圖簡單分析,實際DNS查詢和TCP鏈接創建比較相似,這裏能夠先簡單瞭解,後面會詳細介紹

從上圖咱們能夠了解到的信息有

1> 從上到下分別是應用程序,Socket庫,操做系統協議棧,網卡。。。

2> 瀏覽器要獲取IP地址,先會去調用Socket庫(就是一堆通用程序組件的集合,其餘的應用程序都須要使用其中的組件)中的域名解析器,並預留將返回的結果存在應用程序的內存中。

3> 瀏覽器和Socket庫並不具有使用網絡收發數據的功能,須要使用協議棧執行發送消息的操做, 而後經過網卡將消息發送給 DNS服務器。而且咱們注意到這裏了使用的是UDP協議(後面章節的創建鏈接會講到TCP握手,這裏並非使用TCP協議)。

4> 查詢到後dns服務器會一層一層的返回數據信息。

3 創建TCP鏈接,(4 發送請求,7 斷開鏈接)

這裏會將步驟3 4 7 連起來一塊兒講解,這樣更利於總體把握。

知道了 IP 地址以後, 就能夠委託操做系統內部的協議棧向這個目標 IP 地址, 也就是咱們要訪問的 Web 服務器發送創建鏈接的請求了。要更好的理解這部份內容就要對TCP/IP軟件的分層結構有個認識。

分爲不一樣的層次,分別承擔不一樣的功能。上面部分會向下面部分委派工做,下面部分接受委派並進行實際執行,這一上下關係只是一個整體的規則, 其中也有一部分上下關係不明確, 或者上下關係相反的狀況, 因此也沒必要過於糾結。

能夠按照 Socket庫 -> 協議棧 -> 網卡驅動程序 -> 網卡 -> 網絡 這個大致的思路進行分析。

1> 最上面部分是網絡應用程序,好比瀏覽器,電子郵件客戶端,web服務器等。

2> 而後是Socket庫,前面DNS解析是提過,包括域名解析器,用來向DNS服務器發出查詢,還有其餘一些其餘功能,後面章節會說起。

3> 再下面是操做系統內部,其中包括協議棧,協議棧的上半部分有兩塊, 分別是負責用 TCP 協議收發數據的部分和負責用 UDP 協議收發數據的部分, 它們會接受應用程序的委託執行收發數據的操做。下面一半是用 IP 協議控制網絡包收發操做的部分。 在互聯網上傳送數據時, 數據會被切分紅一個一個的網絡包 , 而將網絡包發送給通訊對象的操做就是由 IP 來負責的。 此外, IP 中還包括ICMP協議和 ARP 協議。ICMP 用於告知網絡包傳送過程當中產生的錯誤以及各類控制消息, ARP 用於根據 IP 地址查詢相應的以太網 MAC 地址 。

4> IP 下面的網卡驅動程序負責控制網卡硬件, 而最下面的網卡則負責完成實際的收發操做, 也就是對網線中的信號執行發送和接收的操做。

3.1 收發數據的全貌

咱們能夠把數據通道想象成一條管道, 將數據從一端送入管道, 數據就會到達管道的另外一端而後被取出。 數據能夠從任何一端被送入管道, 數據的流動是雙向的。 不過, 這並非說現實中真的有這麼一條管道, 只是爲了幫助你們理解數據收發操做的全貌,要注意到這些操做並非瀏覽器等程序完成的,而是由協議棧來代勞的,向操做系統內部的協議棧發出委託時,須要按照指定的順序來調用 Socket 庫中的程序組件。。

關於這條管道,大體總結如下 4 個階段。

( 1)建立套接字(建立套接字階段)

( 2)將管道鏈接到服務器端的套接字上(鏈接階段)

( 3)收發數據(通訊階段)

( 4)斷開管道並刪除套接字(斷開階段)

咱們事先提取幾個關鍵詞,簡單理解下,

1> 應用程序:瀏覽器等。

2> Socket庫:應用程序會經過調用Socket庫向協議棧發出委託。

3> 協議棧:協議棧負責數據的收發工做。

4> 套接字:於管道兩端的數據出入口,協議棧是根據套接字中記錄的控制信息來工做的,在協議棧內部有一塊用於存放控制信息的內存空間, 這裏記錄了用於控制通訊操做的控制信息, 例如通訊對象的 IP 地址、 端口號、 通訊操做的進行狀態等。協議棧則須要根據這些信息判斷下一步的行動。

5> 描述符:套接字的惟一標識,號碼牌

注意:爲了便於總體把握,咱們將這四個階段一塊兒講解。下圖是收發數據的底層原理僞代碼

3.2 建立套接字階段

瀏覽器調用socket庫提出建立套接字申請,而後協議棧根據申請執行建立套接字的操做,這個過程當中,協議棧首先會分配一個用於存放套接字的內存空間,至關於給控制信息準備一個容器,咱們須要往其中寫入控制信息。建立之初,數據收發還沒開始,先向該內存空間寫入初始狀態控制信息,這樣就完成了建立套接字的操做。

在咱們建立套接字的同時,每一個套接字就有了惟一編號,稱之爲描述符,接下來須要將這個套接字的描述符告知應用程序。以後,應用程在向協議棧收發數據委託的時候就須要提供這個描述符,這個描述符能夠肯定相應的套接字,套接字中又有相關信息,這樣一來,只需提供這個描述符,應用程序就不用每次都告訴協議棧應該和誰進行通訊了。

3.3 鏈接階段

套接字建立以後,應用程序會調用connect,隨後協議棧會將本地的套接字和服務器的套接字進行鏈接。在鏈接操做過程當中,還須要提早分配一塊用來臨時存放要收發的數據的內存空間,這塊空間被稱爲緩衝區,該緩衝區在執行數據收發操做的時候要用到。

分別看下客戶端和服務端的狀況

客戶端:套接字建立之初,裏面只有初始信息,不知道通信的對象是誰,這時即使想要發送數據,協議棧也不知道應該將數據發給誰。可是其實應用程序是知道通信對象的,經過DNS域名解析獲得目標服務器的IP地址,同時不一樣的服務佔用服務器不一樣的端口,通常來講這都是預先定好的,這些能幫助咱們找到通信對象。應用程序知道,協議棧不知道通信對象,是由於在調用 socket 建立套接字時, 這些必要信息並無傳遞給協議棧,因此咱們須要將這些信息告訴協議棧,這也是鏈接操做的目的之一。

服務端:一樣也會建立套接字,初始階段也不知道要和誰通信,甚至連服務端應用程序都不知道應該和誰通信,因此通常來講服務器會一直監聽等待客戶端的通信,客戶端在通信的過程當中會帶着通信的必要信息。

3.3.1 調用connect

調用 connect 時,須要指定描述符、服務器 IP 地址和端口號這 3 個參數。

第 1 個參數, 即描述符,connect 會將應用程序指定的描述符告知協議棧, 而後協議棧根據這個描述符來判斷到底使用哪個套接字去和服務器端的套接字進行鏈接, 並執行鏈接的操做。

第 2 個參數, 即服務器 IP 地址, 就是經過 DNS 服務器查詢獲得的咱們要訪問的服務器的 IP 地址。

第 3 個參數, 即端口號,同時指定 IP 地址和端口號時,就能夠明確識別出某臺具體的計算機上的某個具體的套接字。

這裏思考兩個問題

問題1:到目前咱們瞭解到可以識別套接字有兩套機制,一種是描述符,另外一種是IP+端口號,那麼他們有什麼區別呢?

1> 描述符是用來在一臺計算機內部識別套接字的機制, 好比瀏覽器和協議棧之間

2> 那麼IP+端口號就是用來讓通訊的另外一方可以識別出套接字的機制,好比客戶端和服務端之間,實際上web端和服務端又會有不一樣,後面第5章節介紹WEB服務器原理時會講到。

問題2:服務端和客戶端使用端口號的區別?

1> 服務器上所使用的端口號是根據應用的種類事先規定好的, 僅此而已。 好比 Web 是 80 號端口, 電子郵件是 25 號端口。

2> 客戶端在建立套接字時, 協議棧會爲這個套接字隨便分配一個端口號 。 接下來, 當協議棧執行鏈接操做時, 會將這個隨便分配的端口號通知給服務器。

3.3.2 通信雙方交換控制信息

鏈接操做其實是通信雙方交換控制信息,通信操做使用的控制信息大致能夠分爲兩類,

一類就是咱們上面提到的保存在套接字中的信息,用來控制協議棧操做。

還有一類就是客戶端和服務器相互聯絡時交換的控制信息,稱爲TCP頭部信息,這些信息不只鏈接時須要, 包括數據收發和斷開鏈接操做在內, 整個通訊過程當中都須要,這些內容在 TCP 協議的規格中進行了定義。注意區分IP頭部,以太網頭部,TCP頭部是不一樣的頭部。

3.3.3 鏈接操做的實際過程

下面來看一下具體的操做過程。 這個過程是從應用程序調用 Socket 庫的 connect 開始的。

connect( < 描述符 >, < 服務器 IP 地址和端口號 >, …)

上面的調用提供了服務器的 IP 地址和端口號, 這些信息會傳遞給協議棧中的 TCP 模塊。 而後, TCP 模塊會與該 IP 地址對應的對象, 也就是與服務器的 TCP 模塊交換控制信息, 這一交互過程包括下面幾個步驟。

首先, 客戶端先建立一個包含表示開始數據收發操做的控制信息的頭部。 頭部包含不少字段, 這裏要關注的重點是發送方和接收方的端口號。 到這裏, 客戶端(發送方)的套接字就準確找到了服務器(接收方)的套接字, 也就是搞清楚了我應該鏈接哪一個套接字。

而後, 咱們將頭部中的控制位的 SYN 比特設置爲 1,你們能夠認爲它表示鏈接 。 此外還須要設置適當的序號和窗口大小。

當 TCP 頭部建立好以後, 接下來 TCP 模塊會將信息傳遞給 IP 模塊並委託它進行發送 。 IP 模塊執行網絡包發送操做後,網絡包就會經過網絡到達服務器, 而後服務器上的 IP 模塊會將接收到的數據傳遞給 TCP 模塊,服務器的 TCP 模塊根據 TCP 頭部中的信息找到端口號對應的套接字, 也就是說, 從處於等待鏈接狀態的套接字中找到與 TCP 頭部中記錄的端口號相同的套接字就能夠了。 當找到對應的套接字以後, 套接字中會寫入相應的信息, 並將狀態改成正在鏈接 。

上述操做完成後, 服務器的 TCP 模塊會返回響應, 這個過程和客戶端同樣, 須要在 TCP 頭部中設置發送方和接收方端口號以及 SYN 比特。 此外,在返回響應時還須要將 ACK 控制位設爲1, 這表示已經接收到相應的網絡包。 網絡中常常會發生錯誤, 網絡包也會發生丟失, 所以雙方在通訊時必須相互確認網絡包是否已經送達 , 而設置ACK 比特就是用來進行這一確認的。 接下來, 服務器 TCP 模塊會將 TCP頭部傳遞給 IP 模塊, 並委託 IP 模塊向客戶端返回響應。

而後, 網絡包就會返回到客戶端, 經過 IP 模塊到達 TCP 模塊, 並經過 TCP 頭部的信息確認鏈接服務器的操做是否成功。 若是 SYN 爲 1 則表示鏈接成功, 這時會向套接字中寫入服務器的 IP 地址、 端口號等信息, 同時還會將狀態改成鏈接完畢。 到這裏, 客戶端的操做就已經完成,

但其實還剩下最後一個步驟。 剛纔服務器返回響應時將 ACK 比特設置爲 1, 相應地,客戶端也須要將 ACK 比特設置爲 1 併發回服務器, 告訴服務器剛纔的響應包已經收到。 當這個服務器收到這個返回包以後, 鏈接操做纔算所有完成。如今, 套接字就已經進入隨時能夠收發數據的狀態了, 你們能夠認爲這時有一根管子把兩個套接字鏈接了起來。

以上實際就是大名鼎鼎的TCP三次握手,下面是圖解

3.4 通信階段

當套接字鏈接起來以後, 剩下的事情就簡單了。 只要將數據送入套接字, 數據就會被髮送到對方的套接字中。 固然, 應用程序沒法直接控制套接字, 所以仍是要經過 Socket 庫委託協議棧來完成這個操做。 這個操做須要使用 write 這個程序組件,須要指定描述符和發送數據, 而後協議棧就會將數據發送到服務器。 因爲套接字中已經保存了已鏈接的通訊對象的相關信息, 因此只要經過描述符指定套接字, 就能夠識別出通訊對象, 並向其發送數據。

協議棧並不關心應用程序傳來的數據是什麼內容。 應用程序在調用 write 時會指定發送數據的長度, 在協議棧看來, 要發送的數據就是必定長度的二進制字節序列而已。

3.4.1 協議棧什麼時候發送網絡包

協議棧並非一收到數據就立刻發送出去, 而是會將數據存放在內部的發送緩衝區中, 並等待應用程序的下一段數據。 應用程序交給協議棧發送的數據長度是由應用程序自己來決定的,有些程序會一次性傳遞全部的數據,有些程序則會逐字節或者逐行傳遞數據,協議棧並不能控制這一行爲。 若是一收到數據就立刻發送出去, 就可能會發送大量的小包, 致使網絡效率降低, 所以須要在數據積累到必定量時再發送出去。 至於要積累多少數據才能發送, 不一樣種類和版本的操做系統會有所不一樣, 不能一律而論, 但都是根據下面幾個要素來判斷的。

第一個判斷要素是每一個網絡包能容納的數據長度, 協議棧會根據一個叫做 MTUA 的參數來進行判斷。 MTU 表示一個網絡包的最大長度, 在以太網中通常是 1500 字節。 MTU 是包含頭部的總長度, 所以須要從MTU 減去頭部的長度, 而後獲得的長度就是一個網絡包中所能容納的最大數據長度, 這一長度叫做 MSSC。 當從應用程序收到的數據長度超過或者接近 MSS 時再發送出去, 就能夠避免發送大量小包的問題了

另外一個判斷要素是時間。 當應用程序發送數據的頻率不高的時候, 若是每次都等到長度接近 MSS 時再發送, 可能會由於等待時間太長而形成發送延遲, 這種狀況下, 即使緩衝區中的數據長度沒有達到 MSS, 也應該果斷髮送出去。 爲此, 協議棧的內部有一個計時器, 當通過必定時間以後,就會把網絡包發送出去。

判斷要素就是這兩個, 但它們實際上是互相矛盾的。 若是長度優先, 那麼網絡的效率會提升, 但可能會由於等待填滿緩衝區而產生延遲; 相反地,若是時間優先, 那麼延遲時間會變少, 但又會下降網絡的效率。 所以, 在進行發送操做時須要綜合考慮這兩個要素以達到平衡。

協議棧也給應用程序保留了控制發送時機的餘地。 應用程序在發送數據時能夠指定一些選項, 好比若是指定「 不等待填滿緩衝區直接發送」, 則協議棧就會按照要求直接發送數據。 像瀏覽器這種會話型的應用程序在向服務器發送數據時, 等待填滿緩衝區致使延遲會產生很大影響, 所以通常會使用直接發送的選項。

3.4.2 對較大數據進行拆分

HTTP 請求消息通常不會很長, 一個網絡包就能裝得下, 但若是其中要提交表單數據, 長度就可能超過一個網絡包所能容納的數據量,這種狀況下, 發送緩衝區中的數據就會超過 MSS 的長度, 這時咱們固然不須要繼續等待後面的數據了。 發送緩衝區中的數據會被以 MSS 長度爲單位進行拆分, 拆分出來的每塊數據會被放進單獨的網絡包中。

3.4.3使用ACK號確認網絡包已收到

首先, TCP 模塊在拆分數據時,會先算好每一塊數據至關於從頭開始的第幾個字節, 接下來在發送這一塊數據時, 將算好的字節數寫在 TCP 頭部中,「 序號」 字段就是派在這個用場上的。 而後, 發送數據的長度也須要告知接收方, 不過這個並非放在TCP 頭部裏面的, 由於用整個網絡包的長度減去頭部的長度就能夠獲得數據的長度, 因此接收方能夠用這種方法來進行計算。

在實際的通訊中,序號並非從 1 開始的, 由於若是序號都從 1 開始, 通訊過程就會很是容易預測,因此初始值是隨機的,這樣對方就搞不清楚序號究竟是從多少開始計算的, 所以須要在開始收發數據以前將初始值告知通訊對象。 實際上, 鏈接操做中在將 SYN 設爲 1 的同時, 還須要同時設置序號字段的值, 而這裏的值就表明序號的初始值

首先, 客戶端在鏈接時須要計算出與從客戶端到服務器方向通訊相關的序號初始值, 並將這個值發送給服務器(①)。 接下來, 服務器會經過這個初始值計算出 ACK 號並返回給客戶端(②)。 初始值有可能在通訊過程當中丟失, 所以當服務器收到初始值後須要返回 ACK 號做爲確認。 同時, 服務器也須要計算出與從服務器到客戶端方向通訊相關的序號初始值, 並將這個值發送給客戶端(圖 2.9 ②)。 接下來像剛纔同樣, 客戶端也須要根據服務器發來的初始值計算出 ACK 號並返回給服務器( ③)。 到這裏, 序號和 ACK 號都已經準備完成了,實際上這也是上面提到的TCP三次握手的內容。

接下來就能夠進入數據收發階段了。 數據收發操做自己是能夠雙向同時進行的, 但 Web 中是先由客戶端向服務器發送請求, 序號也會跟隨數據一塊兒發送( ④)。 而後, 服務器收到數據後再返回 ACK 號(⑤)。 從服務器向客戶端發送數據的過程則正好相反(⑥⑦)。

經過「序號」和「ACK 號」能夠確認接收方是否收到了網絡包。TCP 採用這樣的方式確認對方是否收到了數據, 在獲得對方確認以前, 發送過的包都會保存在發送緩衝區中。 若是對方沒有返回某些包對應的 ACK 號, 那麼就從新發送這些包。

3.4.4 根據網絡包平均往返時間調整 ACK 號等待時間

TCP 採用了動態調整等待時間的方法, 這個等待時間是根據 ACK 號返回所需的時間來判斷的。 具體來講, TCP 會在發送數據的過程當中持續測量 ACK 號的返回時間, 若是 ACK 號返回變慢, 則相應延長等待時間; 相對地, 若是 ACK 號立刻就能返回, 則相應縮短等待時間。

3.4.5 使用窗口有效管理 ACK 號

每發送一個包就等待一個 ACK 號的方式是最簡單也最容易理解的, 但在等待 ACK 號的這段時間中, 若是什麼都不作那實在太浪費了。 爲了減小這樣的浪費, TCP 採用滑動窗口方式來管理數據發送和 ACK 號的操做。 所謂滑動窗口, 就是在發送一個包以後, 不等待 ACK 號返回, 而是直接發送後續的一系列包。 這樣一來, 等待 ACK 號的這段時間就被有效利用起來了。

這種方式效率高,可是也會有個問題,若是不等返回 ACK 號就連續發送包, 就有可能會出現發送包的頻率超過接收方處理能力的狀況。能夠經過下面的方法來避免這種狀況的發生。 首先, 接收方須要告訴發送方本身最多能接收多少數據,而後發送方根據這個值對數據發送操做進行控制, 這就是滑動窗口方式的基本思路。圖中, 接收方將數據暫存到接收緩衝區中並執行接收操做。 當接收操做完成後, 接收緩衝區中的空間會被釋放出來, 也就能夠接收更多的數據了,這時接收方會經過 TCP 頭部中的窗口字段將本身能接收的數據量告知發送方。 這樣一來, 發送方就不會發送過多的數據, 致使超出接收方的處理能力了。

3.4.6 ACK 與窗口的合併

接收方在發送 ACK 號和窗口更新時, 並不會立刻把包發送出去, 而是會等待一段時間, 在這個過程當中頗有可能會出現其餘的通知操做,這樣就能夠把兩種通知合併在一個包裏面發送了。當須要連續發送多個 ACK 號時, 也能夠減小包的數量, 這是由於 ACK 號表示的是已收到的數據量, 也就是說, 它是告訴發送方目前已接收的數據的最後位置在哪裏, 所以當須要連續發送 ACK 號時, 只要發送最後一個 ACK 號就能夠了, 中間的能夠所有省略。當須要連續發送多個窗口更新時也能夠減小包的數量, 由於連續發生窗口更新說明應用程序連續請求了數據, 接收緩衝區的剩餘空間連續增長。 這種狀況和 ACK 號同樣, 能夠省略中間過程, 只要發送最終的結果就能夠了。

3.4.7 接收HTTP響應消息

當消息返回後, 須要執行的是接收消息的操做。 接收消息的操做是經過 Socket 庫中的 read 程序組件委託協議棧來完成的。 首先, 協議棧嘗試從接收緩衝區中取出數據並傳遞給應用程序, 但這個時候請求消息剛剛發送出去, 響應消息可能還沒返回。 響應消息的返回還須要等待一段時間, 所以這時接收緩衝區中並無數據, 那麼接收數據的操做也就沒法繼續。 這時, 協議棧會將應用程序的委託, 也就是從接收緩衝區中取出數據並傳遞給應用程序的工做暫時掛起, 等服務器返回的響應消息到達以後再繼續執行接收操做。

協議棧接收數據的具體操做過程簡單總結一下 。 首先,協議棧會檢查收到的數據塊和 TCP 頭部的內容, 判斷是否有數據丟失, 若是沒有問題則返回 ACK 號。 而後,協議棧將數據塊暫存到接收緩衝區中, 並將數據塊按順序鏈接起來還原出原始的數據, 最後將數據交給應用程序。 具體來講, 協議棧會將接收到的數據複製到應用程序指定的內存地址中, 而後將控制流程交回應用程序。將數據交給應用程序以後, 協議棧還須要找到合適的時機向發送方發送窗口更新。

3.5 斷開階段

收發數據結束的時間點應該是應用程序判斷全部數據都已經發送完畢的時候。協議棧在設計上容許任何一方先發起斷開過程,客戶端和服務端均可以斷開鏈接。不管哪一種狀況, 完成數據發送的一方會發起斷開過程, 這裏咱們以服務器一方發起斷開過程爲例來進行講解。

第一次揮手(FIN=1,seq=u)
假設客戶端想要關閉鏈接,客戶端發送一個 FIN 標誌位置爲1的包,表示本身已經沒有數據能夠發送了,可是仍然能夠接受數據。
發送完畢後,客戶端進入 FIN_WAIT_1 狀態。
第二次揮手(ACK=1,ACKnum=u+1)
服務器端確認客戶端的 FIN包,發送一個確認包,代表本身接受到了客戶端關閉鏈接的請求,但尚未準備好關閉鏈接。
發送完畢後,服務器端進入 CLOSE_WAIT 狀態,客戶端接收到這個確認包以後,進入 FIN_WAIT_2 狀態,等待服務器端關閉鏈接。
第三次揮手(FIN=1,seq=w)
服務器端準備好關閉鏈接時,向客戶端發送結束鏈接請求,FIN置爲1。
發送完畢後,服務器端進入 LAST_ACK 狀態,等待來自客戶端的最後一個ACK。
第四次揮手(ACK=1,ACKnum=w+1)
客戶端接收到來自服務器端的關閉請求,發送一個確認包,並進入 TIME_WAIT狀態,等待可能出現的要求重傳的 ACK包。
服務器端接收到這個確認包以後,關閉鏈接,進入 CLOSED 狀態。
客戶端等待了某個固定時間(兩個最大段生命週期,2MSL,2 Maximum Segment Lifetime)以後,沒有收到服務器端的 ACK,認爲服務器端已經正常關閉鏈接,因而本身也關閉鏈接,進入 CLOSED狀態。

網絡上比較主流的文章都說關閉TCP會話是四次揮手,可是實際上爲了提升效率一般合併第2、三次的揮手,即三次揮手。

和服務器的通訊結束以後, 用來通訊的套接字也就不會再使用了, 這時咱們就能夠刪除這個套接字了。 不過, 套接字並不會當即被刪除, 而是會等待一段時間以後再被刪除,等待這段時間是爲了防止誤操做, 引起誤操做的緣由有不少。

好比,最後客戶端返回的 ACK 號丟失了, 結果會如何呢? 這時, 服務器沒有接收到 ACK 號, 可能會重發一次 FIN。 若是這時客戶端的套接字已經刪除了, 會發生什麼事呢? 套接字被刪除, 那麼套接字中保存的控制信息也就跟着消失了, 套接字對應的端口號就會被釋放出來。 這時, 若是別的應用程序要建立套接字, 新套接字碰巧又被分配了同一個端口號,而服務器重發的 FIN 正好到達, 會怎麼樣呢? 原本這個 FIN 是要發給剛剛刪除的那個套接字的, 但新套接字具備相同的端口號, 因而這個 FIN 就會錯誤地跑到新套接字裏面, 新套接字就開始執行斷開操做了。 之因此不立刻刪除套接字, 就是爲了防止這樣的誤操做。

3.7 小結

最後總體回顧下這一章節講的TCP收據收發全過程

數據收發操做的第一步是建立套接字。 通常來講, 服務器一方的應用程序在啓動時就會建立好套接字並進入等待鏈接的狀態。 客戶端則通常是在用戶觸發特定動做, 須要訪問服務器的時候建立套接字。 在這個階段,尚未開始傳輸網絡包。

建立套接字以後, 客戶端會向服務器發起鏈接操做。 首先, 客戶端會生成一個 SYN 爲 1 的 TCP 包併發送給服務器(圖 2.13 ①)。 這個 TCP 包的頭部還包含了客戶端向服務器發送數據時使用的初始序號, 以及服務器向客戶端發送數據時須要用到的窗口大小 A。 當這個包到達服務器以後, 服務器會返回一個 SYN 爲 1 的 TCP 包(圖 2.13 ②)。 和圖 2.13 ①同樣, 這個包的頭部中也包含了序號和窗口大小, 此外還包含表示確認已收到包①的ACK 號 B。 當這個包到達客戶端時,客戶端會向服務器返回一個包含表示確認的 ACK 號的 TCP 包(圖 2.13 ③)。 到這裏, 鏈接操做就完成了, 雙方進入數據收發階段。

數據收發階段的操做根據應用程序的不一樣而有一些差別, 以 Web 爲例, 首先客戶端會向服務器發送請求消息。 TCP 會將請求消息切分紅必定大小的塊, 並在每一塊前面加上 TCP 頭部, 而後發送給服務器(圖 2.13 ④)。TCP 頭部中包含序號, 它表示當前發送的是第幾個字節的數據。 當服務器收到數據時, 會向客戶端返回 ACK 號(圖 2.13 ⑤)。 在最初的階段, 服務器只是不斷接收數據, 隨着數據收發的進行, 數據不斷傳遞給應用程序,接收緩衝區就會被逐步釋放。 這時, 服務器須要將新的窗口大小告知客戶端。 當服務器收到客戶端的請求消息後, 會向客戶端返回響應消息, 這個過程和剛纔的過程正好相反(圖 2.13 ⑥⑦)。

服務器的響應消息發送完畢以後, 數據收發操做就結束了, 這時就會開始執行斷開操做。 以 Web 爲例,服務器會先發起斷開過程 A。 在這個過程當中, 服務器先發送一個 FIN 爲 1 的 TCP 包(圖 2.13 ⑧), 而後客戶端返回一個表示確認收到的 ACK 號(圖 2.13 ⑨)。 接下來, 雙方還會交換一組方向相反的 FIN 爲 1 的 TCP 包(圖 2.13 ⑩)和包含 ACK 號的 TCP 包(圖 2.13k)。最後, 在等待一段時間後, 套接字會被刪除。

5 WEB服務器

5.1 服務器程序的結構

服務器須要同時和多個客戶端通訊, 但一個程序來處理多個客戶端的請求是很難的, 由於服務器必須把握每個客戶端的操做狀態。 所以通常的作法是, 每有一個客戶端鏈接進來, 就啓動一個新的服務器程序, 確保服務器程序和客戶端是一對一的狀態。

具體來講, 服務器程序的結構如圖 6.1 所示。 首先, 咱們將程序分紅兩個模塊, 即等待鏈接模塊(圖 6.1( a))和負責與客戶端通訊的模塊(圖6.1 ( b))A。 當服務器程序啓動並讀取配置文件完成初始化操做後,就會運行等待鏈接模塊( a)。 這個模塊會建立套接字, 而後進入等待鏈接的暫停狀態。 接下來, 當客戶端連發起鏈接時, 這個模塊會恢復運行並接受鏈接,而後啓動客戶端通訊模塊( b), 並移交完成鏈接的套接字。 接下來, 客戶端通訊模塊( b)就會使用已鏈接的套接字與客戶端進行通訊, 通訊結束後,這個模塊就退出了。

每次有新的客戶端發起鏈接, 都會啓動一個新的客戶端通訊模塊( b),所以( b)與客戶端是一對一的關係。 這樣,( b)在工做時就沒必要考慮其餘客戶端的鏈接狀況, 只要關心本身對應的客戶端就能夠了。 經過這樣的方式, 能夠下降程序編寫的難度。 服務器操做系統具備多任務 A、 多線程 B 功能, 能夠同時運行多個程序 C, 服務器程序的設計正是利用了這一功能。

固然, 這種方法在每次客戶端發起鏈接時都須要啓動新的程序, 這個過程比較耗時, 響應時間也會相應增長。 所以, 還有一種方法是事先啓動幾個客戶端通訊模塊, 當客戶端發起鏈接時, 從空閒的模塊中挑選一個出來將套接字移交給它來處理。後面會研究nginx服務器的工做方式。

5.2 服務端的具體工做過程

僞代碼以下

首先, 協議棧調用 socket 建立套接字(圖 6.2( 1)), 這一步和客戶端是相同的。

接下來調用 bind 將端口號寫入套接字中(圖 6.2( 2-1))。 在客戶端發起鏈接的操做中, 須要指定服務器端的端口號。設置好端口號以後, 協議棧會調用 listen 向套接字寫入等待鏈接狀態這一控制信息(圖 6.2( 2-1))。 這樣一來, 套接字就會開始等待來自客戶端的鏈接網絡包。

而後, 協議棧會調用 accept 來接受鏈接(圖 6.2( 2-2))。 因爲等待鏈接的模塊在服務器程序啓動時就已經在運行了, 因此在剛啓動時, 應該尚未客戶端的鏈接包到達。 但是, 包都沒來就調用 accept 接受鏈接, 可能你們會感到有點奇怪, 不過不要緊, 由於若是包沒有到達, 就會轉爲等待包到達的狀態, 並在包到達的時候繼續執行接受鏈接操做。 所以, 在執行accept 的時候, 通常來講服務器端都是處於等待包到達的狀態, 這時應用程序會暫停運行。 在這個狀態下, 一旦客戶端的包到達, 就會返回響應包並開始接受鏈接操做。 接下來, 協議棧會給等待鏈接的套接字複製一個副本, 而後將鏈接對象等控制信息寫入新的套接字中(圖 6.3)。 剛纔咱們介紹了調用 accept 時的工做過程, 到這裏, 咱們就建立了一個新的套接字,並和客戶端套接字鏈接在一塊兒了。

當 accept 結束以後, 等待鏈接的過程也就結束了, 這時等待鏈接模塊會啓動客戶端通訊模塊, 而後將鏈接好的新套接字轉交給客戶端通訊模塊,由這個模塊來負責執行與客戶端之間的通訊操做。 以後的數據收發操做和剛纔說的同樣, 與客戶端的工做過程是相同的。

問題1:在複製出一個新的套接字以後, 原來那個處於等待鏈接狀態的套接字會怎麼樣呢?

其實它還會以等待鏈接的狀態繼續存在, 當再次調用 accept, 客戶端鏈接包到達時, 它又能夠再次執行接受鏈接操做。

問題2:服務端新建立的套接字副本和原來的等待鏈接的套接字具備相同的端口號,這樣不會有問題嗎?

端口號是用來識別套接字的, 可是,實際上服務端新建立的套接字副本和原來的等待鏈接的套接字具備相同的端口號。然而這並不會有問題,要肯定某個套接字時, 不只使用服務器端套接字對應的IP地址和端口號, 還同時使用客戶端的端口號再加上 IP 地址, 總共使用下面 4 種信息來進行判斷。服務器上可能存在多個端口號相同的套接字, 但客戶端的套接字都是對應不一樣端口號的, 所以咱們能夠經過客戶端的端口號來肯定服務器上的某個套接字。

5.3 nginx服務器是如何接收消息的

Nginx 採用的是多進程(單線程) & 多路IO複用模型。使用了 I/O 多路複用技術的 Nginx,就成了」併發事件驅動「的服務器。這裏咱們探討下和上面聯繫比較緊密的多進程工做模式。

咱們看下nginx的多進程工做模式

一、Nginx 在啓動後,會有一個 master 進程和多個相互獨立的 worker 進程。
二、接收來自外界的信號,向各worker進程發送信號,每一個進程都有可能來處理這個鏈接。
三、 master 進程能監控 worker 進程的運行狀態,當 worker 進程退出後(異常狀況下),會自動啓動新的 worker 進程。

使用多進程模式,不只能提升併發率,並且進程之間相互獨立,一個 worker 進程掛了不會影響到其餘 worker 進程。

5.3.1 驚羣現象

主進程(master 進程)首先經過 socket() 來建立一個 sock 文件描述符用來監聽,而後fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),以後子進程 accept() 後將建立已鏈接描述符(connected descriptor)),而後經過已鏈接描述符來與客戶端通訊。
那麼,因爲全部子進程都繼承了父進程的 sockfd,那麼當鏈接進來時,全部子進程都將收到通知並「爭着」與它創建鏈接,這就叫「驚羣現象」。大量的進程被激活又掛起,只有一個進程能夠accept() 到這個鏈接,這固然會消耗系統資源。

5.3.2 Nginx對驚羣現象的處理

Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把共享鎖。即每一個 worker 進程在執行 accept 以前都須要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖以後,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚羣問題了。accept_mutex 是一個可控選項,咱們能夠顯示地關掉,默認是打開的。

6 語言解析:PHP-FPM

6.1 nginx和php-fpm配合工做

nginx服務器起得做用其實是內容分發,咱們會有不少請求,好比咱們請求一個靜態圖片,只須要找到相應目錄下的文件便可,可是若是有個php請求,那麼這就超出了nginx的能力範圍,須要有人專門處理php請求。

若是你有這方面經驗,那麼在nginx的虛擬主機配置中有這麼一段

21     location ~ \.php$ {
 22         fastcgi_pass 127.0.0.1:9000;
 23         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
 24         include fastcgi_params;
 25     }

這段配置的意思就是,若是是php請求,那麼nginx就將請求發送給本機的9000端口處理,關於fastcgi_params的兩句是定義nginx變量和fastcgi變量的關係,在/etc/nginx/目錄下會有個 fastcgi_params文件,能夠打開看下,比較簡單。

這裏的主角其實就是監聽9000端口的php-fpm,這個端口是能夠經過配置文件本身定義的,通常默認使用9000,service php-fpm start 運行後,查看9000端口狀況

$ netstat -anlp | grep 9000
tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      750/php-fpm: master

6.2 cgi,fastcgi,php-cgi,php-fpm的關係

講到這裏,咱們要了解下這段歷史,區分幾個概念

1> CGI

CGI全稱是「公共網關接口」(Common Gateway Interface),HTTP服務器與你的或其它機器上的程序進行「交談」的一種工具,其程序須運行在網絡服務器上。

CGI能夠用任何一種語言編寫,只要這種語言具備標準輸入、輸出和環境變量。如php,perl,tcl等。

服務器接收到請求後,若是是index.html這樣的靜態文件,能夠直接去相應的目錄找到這個文件,而後返回給客戶端,可是當發送的請求是index.php這樣請求,顯然這個是須要解析的,此時就須要服務器將這個請求傳遞給cgi程序解析,解析完成後返回結果。可是要傳遞什麼內容呢,這個就是cgi來規定的。

2> Fastcgi

Fastcgi是用來提升CGI程序性能的,是CGI的升級版,一種語言無關的協議

服務器每次將請求傳遞給cig程序解析的時候都會解析配置文件,好比php.ini,想一想就知道這會影響性能,因此單獨使用上面提到的CGI程序會很慢。fastcgi會先啓動一個master解析配置文件,初始化環境,而後再啓動多個worker,當請求過來的時候master會傳遞給worker,而後當即去接受下一個請求。當worker不夠用的時候會增長,當空閒的worker多的時候會停掉一些,動態增減worker的機制能夠提升性能,節省資源。

3> php-cgi

PHP-CGI是php自帶的Fast-CGI管理器.
php.ini修改以後,必須kill掉php-cgi再啓動php.ini 才生效。不能夠平滑的重啓
內存不能動態分配
啓動php,指定啓動的worker ,長期駐留在內存裏 ,用戶訪問php文件, php-cgi 處理請求,返回結果

4> Php-fmp

PHP-FPM 是 PHP 針對 FastCGI 協議的具體實現,非官方fastCgi進程管理器,後來php5.4開始,被官方收錄了
能夠平滑重啓php
動態調度進程
啓動php,動態指定啓動的worker ,長期駐留在內存裏 ,根據來訪壓力動態增減worker的進程數量,用戶訪問php文件, php-fpm 處理請求,返回結果

php-cgi和php-fpm的關係呢

php54是以前是一種關係,php54以後另外一種關係。php54以前,php-fpm(第三方編譯)是管理器,php-cgi是解釋器。php54以後,php-fpm(官方自帶),master 與 pool 模式。php-fpm 和 php-cgi 沒有關係了。php-fpm又是解釋器,又是管理器。網上大部分說法:php-fpm 是管理php-cgi 的,是針對php54以前的

6.3 php-fpm 的配置

配置文件位於/etc/php-fpm.conf (或者是該文件include /etc/php-fpm.d/文件夾下的配置)

pm = dynamic #指定進程管理方式,有3種可供選擇:static、dynamic和ondemand。
pm.max_children = 16 #static模式下建立的子進程數或dynamic模式下同一時刻容許最大的php-fpm子進程數量。
pm.start_servers = 10 #動態方式下的起始php-fpm進程數量。
pm.min_spare_servers = 8 #動態方式下服務器空閒時最小php-fpm進程數量。
pm.max_spare_servers = 16 #動態方式下服務器空閒時最大php-fpm進程數量。
pm.max_requests = 2000 #php-fpm子進程能處理的最大請求數。
pm.process_idle_timeout = 10s
request_terminate_timeout = 120

pm三種進程管理模式說明以下:

  • pm = static,始終保持一個固定數量的子進程,這個數由pm.max_children定義,這種方式很不靈活,也一般不是默認的。

  • pm = dynamic,啓動時會產生固定數量的子進程(由pm.start_servers控制)能夠理解成最小子進程數,而最大子進程數則由pm.max_children去控制,子進程數會在最大和最小數範圍中變化。閒置的子進程數還能夠由另2個配置控制,分別是pm.min_spare_servers和pm.max_spare_servers。若是閒置的子進程超出了pm.max_spare_servers,則會被殺掉。小於pm.min_spare_servers則會啓動進程(注意,pm.max_spare_servers應小於pm.max_children)。

  • pm = ondemand,這種模式和pm = dynamic相反,把內存放在第一位,每一個閒置進程在持續閒置了pm.process_idle_timeout秒後就會被殺掉,若是服務器長時間沒有請求,就只會有一個php-fpm主進程。弊端是遇到高峯期或者若是pm.process_idle_timeout的值過短的話,容易出現504 Gateway Time-out錯誤,所以pm = dynamic和pm = ondemand誰更適合視實際狀況而定。

static管理模式適合比較大內存的服務器,而dynamic則適合小內存的服務器,你能夠設置一個pm.min_spare_servers和pm.max_spare_servers合理範圍,這樣進程數會不斷變更。ondemand模式則更加適合微小內存,例如512MB或者256MB內存,以及對可用性要求不高的環境。

我安裝的php-fpm默認配置文件,

pm = dynamic
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
;pm.max_requests = 500

注意pm.max_requests這句實際上是被註釋掉的,默認值是0,也就是不限制子進程請求次數。

如下是我啓動php-fpm後進程狀況,啓動了一個master進程,和5個依託於該master進程的worker進程。

$ ps -ef | grep php-fpm  
root     28212     1  0 09:12 ?        00:00:00 php-fpm: master process (/etc/php-fpm.conf)
apache   28214 28212  0 09:12 ?        00:00:00 php-fpm: pool www
apache   28215 28212  0 09:12 ?        00:00:00 php-fpm: pool www
apache   28216 28212  0 09:12 ?        00:00:00 php-fpm: pool www
apache   28217 28212  0 09:12 ?        00:00:00 php-fpm: pool www
apache   28218 28212  0 09:12 ?        00:00:00 php-fpm: pool www
root     28226 28103  0 09:12 pts/1    00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn php-fpm

內存佔用太高問題

PHP 進程自己並不存在內存泄露的問題,每一個進程完成請求處理後會回收內存,可是並不會釋放給操做系統,這就致使大量內存被 PHP-FPM 佔用而沒法釋放,請求量升高時性能驟降。

  • 1.考慮設置合適的進程數

按照php-fpm進程數=內存/2/30來計算,1GB內存適合的php-fpm進程數爲10-20之間,具體還得根據你的PHP加載的附加組件有關係。

  • 2.考慮每一個進程佔用內存大小

PHP-FPM 須要控制單個子進程請求次數的閾值。不少人會誤覺得 max_requests 控制了進程的併發鏈接數,實際上 PHP-FPM 模式下的進程是單一線程的,請求沒法併發。這個參數的真正意義是提供請求計數器的功能,超過閾值數目後自動回收,緩解內存壓力

減小pm.max_requests數,當一個 PHP-CGI 進程處理的請求數累積到 max_requests 個後,自動重啓該進程,這樣達到了釋放內存的目的了。

相關文章
相關標籤/搜索