咱們將探索操做系統中的網絡控制軟件(協議棧)和網絡硬件(網卡)是如何將瀏覽器的消息發送給服務器的。瀏覽器
和瀏覽器不一樣的是,協議棧的工做咱們從表面上是看不見的,可能比較不可思議。所以,在實際探索以前,咱們先來對協議棧作個解剖,看看裏面到底有些什麼。緩存
協議棧的內部如圖所示,分爲幾個部分,分別承擔不一樣的功能。這張圖中的上下關係是有必定規則的,上面的部分會向下面的部分委派工做,下面的部分接受委派的工做並實際執行,這一點你們在看圖時能夠參考一下。固然,這一上下關係只是一個整體的規則,其中也有一部分上下關係不明確,或者上下關係相反的狀況,因此也沒必要過於糾結。服務器
上層會向下層逐層委派工做。圖中最上面的部分是網絡應用程序,也就是瀏覽器、電子郵件客戶端、Web服務器、電子郵件服務器等程序,它們會將收發數據等工做委派給下層的部分來完成。應用程序的下面是Socket庫,其中包括解析器,解析器用來向DNS服務器發出查詢。再下面就是操做系統內部了,其中包括協議棧。協議棧的上半部分有兩塊,分別是負責用TCP協議收發數據的部分和負責用UDP協議收發數據的部分,它們會接受應用程序的委託執行收發數據的操做。像瀏覽器、郵件等通常的應用程序都是使用TCP收發數據的,而像DNS查詢等收發較短的控制數據的時候則使用UDP。網絡
下面一半是用IP協議控制網絡包收發操做的部分。在互聯網上傳送數據時,數據會被切分紅一個一個的網絡包,而將網絡包發送給通訊對象的操做就是由IP協議來負責的。IP下面的網卡驅動程序負責控制網卡硬件,而最下面的網卡則負責完成實際的收發操做,也就是對網線中的信號執行發送和接收的操做。併發
咱們已經瞭解了協議棧的內部結構,而對於在數據收發中扮演關鍵角色的套接字,讓咱們來看一看它具體是個怎樣的東西。socket
在協議棧內部有一塊用於存放控制信息的內存空間,這裏記錄了用於控制通訊操做的控制信息,例如通訊對象的IP地址、端口號、通訊操做的進行狀態等。原本套接字就只是一個概念而已,並不存在實體,若是必定要賦予它一個實體,咱們能夠說這些控制信息就是套接字的實體,或者說存放控制信息的內存空間就是套接字的實體。函數
套接字中記錄了用於控制通訊操做的各類控制信息,協議棧則須要根據這些信息判斷下一步的行動,這就是套接字的做用。大數據
在Windows中能夠用netstat命令顯示套接字內容,圖中每一行至關於一個套接字。spa
好比第8行,它表示PID爲4的程序正在使用IP地址爲10.10.1.16的網卡與IP地址爲10.10.1.80的對象進行通訊。此外咱們還能夠看出,本機使用1031端口,對方使用139端口。咱們再來看第1行,這一行表示PID爲984的程序正在135端口等待另外一方的鏈接,其中本地IP地址和遠程IP地址都是0.0.0.0,這表示通訊還沒開始,IP地址不肯定。操作系統
咱們的探索之旅將繼續前進,看一看當瀏覽器調用socket、connect等Socket庫中的函數時,協議棧內部是如何工做的。
首先是建立套接字的階段。應用程序調用socket申請建立套接字,協議棧根據應用程序的申請執行建立套接字的操做。
在這個過程當中,協議棧首先會分配用於存放一個套接字所需的內存空間。套接字剛剛建立時,數據收發操做尚未開始,所以須要在套接字的內存空間中寫入表示這一初始狀態的控制信息。到這裏,建立套接字的操做就完成了。
接下來,須要將表示這個套接字的描述符告知應用程序。收到描述符以後,應用程序在向協議棧進行收發數據委託時就須要提供這個描述符。因爲套接字中記錄了通訊雙方的信息以及通訊處於怎樣的狀態,因此只要經過描述符肯定了相應的套接字,協議棧就可以獲取全部的相關信息,這樣一來,應用程序就不須要每次都告訴協議棧應該和誰進行通訊了。
建立套接字以後,應用程序(瀏覽器)就會調用connect,隨後協議棧會將本地的套接字與服務器的套接字進行鏈接。
那麼這裏的「鏈接」究竟是什麼意思呢?一句話歸納的話,鏈接其實是通訊雙方交換控制信息,在套接字中記錄這些必要信息並準備數據收發的一連串操做。
套接字剛剛建立完成的時候,裏面並無存聽任何數據,也不知道通訊的對象是誰。所以,咱們須要把服務器的IP地址和端口號等信息告知協議棧,這是鏈接操做的目的之一。
那麼,服務器這邊又是怎樣的狀況呢?服務器上也會建立套接字,但服務器上的協議棧和客戶端同樣,只建立套接字是不知道應該和誰進行通訊的。並且,和客戶端不一樣的是,在服務器上,連應用程序也不知道通訊對象是誰。因而,咱們須要讓客戶端向服務器告知必要的信息。可見,客戶端向服務器傳達開始通訊的請求,也是鏈接操做的目的之一。
此外,當執行數據收發操做時,咱們還須要一塊用來臨時存放要收發的數據的內存空間,這塊內存空間稱爲緩衝區,它也是在鏈接操做的過程當中分配的。上面這些就是「鏈接」這個詞表明的具體含義。
關於控制信息,這裏再補充一些。以前咱們說的控制信息其實能夠大致上分爲兩類。
第一類是客戶端和服務器相互聯絡時交換的控制信息。這些信息不只鏈接時須要,包括數據收發和斷開鏈接操做在內,整個通訊過程當中都須要,這些內容在TCP協議的規格中進行了定義。具體來講,下表中的這些字段就是TCP規格中定義的控制信息。
這些字段是固定的,在鏈接、收發、斷開等各個階段中,每次客戶端和服務器之間進行通訊時,都須要提供這些控制信息。具體來講,如圖(a)所示,這些信息會被添加在客戶端與服務器之間傳遞的網絡包的開頭。在鏈接階段,因爲數據收發尚未開始,因此如圖(b)所示,網絡包中沒有實際的數據,只有控制信息。這些控制信息位於網絡包的開頭,所以被稱爲頭部。
控制信息還有另一類,那就是保存在套接字中,用來控制協議棧操做的信息。應用程序傳遞來的信息以及從通訊對象接收到的信息都會保存在這裏,還有收發數據操做的執行狀態等信息也會保存在這裏,協議棧會根據這些信息來執行每一步的操做。
鏈接操做的第一步是客戶端的TCP模塊建立表示鏈接控制信息的頭部,經過TCP頭部中的發送方和接收方端口號能夠找到要鏈接的套接字。而後,咱們將頭部中的控制位的SYN比特設置爲1,目前你們能夠認爲它是表示鏈接。此外還須要設置適當的序號和窗口大小。
當TCP頭部建立好以後,接下來TCP模塊會將信息傳遞給IP模塊並委託它進行發送。IP模塊執行網絡包發送操做後,網絡包就會經過網絡到達服務器,而後服務器上的IP模塊會將接收到的數據傳遞給TCP模塊,服務器的TCP模塊根據TCP頭部中的信息找到端口號對應的套接字。也就是說,從處於等待鏈接狀態的套接字中找到與TCP頭部中記錄的接收方端口號相同的套接字就能夠了。當找到對應的套接字以後,套接字中會寫入相應的信息,並將狀態改成正在鏈接。
上述操做完成後,服務器的TCP模塊會返回響應,這個過程和客戶端同樣,須要在TCP頭部中設置發送方和接收方端口號以及SYN比特。此外,在返回響應時還須要將ACK控制位設爲1,這表示已經接收到相應的網絡包。接下來,服務器TCP模塊會將TCP頭部傳遞給IP模塊,並委託IP模塊向客戶端返回響應。
而後,網絡包就會返回到客戶端,經過IP模塊到達TCP模塊,並經過TCP頭部的信息確認鏈接服務器的操做是否成功。若是ACK爲1則表示鏈接成功,這時會向套接字中寫入服務器的IP地址、端口號等信息,同時還會將狀態改成鏈接完畢。相應地,客戶端也須要將ACK比特設置爲1併發回服務器,告訴服務器剛纔的響應包已經收到。當這個服務器收到這個返回包以後,鏈接操做纔算所有完成。
當控制流程從connect回到應用程序以後,接下來就進入數據收發階段了。
數據收發操做是從應用程序調用write將要發送的數據交給協議棧開始的,協議棧收到數據後執行發送操做。應用程序在調用write時會指定發送數據的長度,在協議棧看來,要發送的數據就是必定長度的二進制字節序列而已。
協議棧並非一收到數據就立刻發送出去,而是會將數據存放在內部的發送緩衝區中,並等待應用程序的下一段數據。一次將多少數據交給協議棧是由應用程序自行決定的,協議棧並不能控制這一行爲。在這樣的狀況下,若是一收到數據就立刻發送出去,就可能會發送大量的小包,致使網絡效率降低,所以須要在數據積累到必定量時再發送出去。至於要積累多少數據才能發送,不一樣種類和版本的操做系統會有所不一樣,不能一律而論,但都是根據下面幾個要素來判斷的。
第一個判斷要素是每一個網絡包能容納的數據長度,協議棧會根據一個叫做MTU的參數來進行判斷。MTU表示一個網絡包的最大長度,在以太網中通常是1500字節。MTU是包含頭部的總長度,所以須要從MTU減去頭部的長度,而後獲得的長度就是一個網絡包中所能容納的最大數據長度,這一長度叫做MSS。當從應用程序收到的數據長度超過或者接近MSS時再發送出去,就能夠避免發送大量小包的問題了。
另外一個判斷要素是時間。當應用程序發送數據的頻率不高的時候,若是每次都等到長度接近MSS時再發送,可能會由於等待時間太長而形成發送延遲,這種狀況下,即使緩衝區中的數據長度沒有達到MSS,也應該果斷髮送出去。爲此,協議棧的內部有一個計時器,當通過必定時間以後,就會把網絡包發送出去。
判斷要素就是這兩個,但它們實際上是互相矛盾的。若是長度優先,那麼網絡的效率會提升,但可能會由於等待填滿緩衝區而產生延遲;相反地,若是時間優先,那麼延遲時間會變少,但又會下降網絡的效率。所以,在進行發送操做時須要綜合考慮這兩個要素以達到平衡。
協議棧也給應用程序保留了控制發送時機的餘地。應用程序在發送數據時能夠指定一些選項,好比若是指定「不等待填滿緩衝區直接發送」,則協議棧就會按照要求直接發送數據。像瀏覽器這種會話型的應用程序在向服務器發送數據時,等待填滿緩衝區致使延遲會產生很大影響,所以通常會使用直接發送的選項。
HTTP請求消息通常不會很長,一個網絡包就能裝得下,但若是其中要提交表單數據,長度就可能超過一個網絡包所能容納的數據量。這種狀況下,發送緩衝區中的數據就會超過MSS的長度,這時咱們固然不須要繼續等待後面的數據了。發送緩衝區中的數據會被以MSS長度爲單位進行拆分,拆分出來的每塊數據會被放進單獨的網絡包中。
根據發送緩衝區中的數據拆分的狀況,當判斷須要發送這些數據時,就在每一塊數據前面加上TCP頭部,並根據套接字中記錄的控制信息標記發送方和接收方的端口號,而後交給IP模塊來執行發送數據的操做。
TCP具有確認對方是否成功收到網絡包,以及當對方沒收到時進行重發的功能,所以在發送網絡包以後,接下來還須要進行確認操做。
咱們先來看一下確認的原理,如圖。首先,TCP模塊在拆分數據時,會先算好每一塊數據至關於從頭開始的第幾個字節,接下來在發送這一塊數據時,將算好的字節數寫在TCP頭部中,「序號」字段就是派在這個用場上的。而後,發送數據的長度也須要告知接收方,不過這個並非放在TCP頭部裏面的,由於用整個網絡包的長度減去頭部的長度就能夠獲得數據的長度,因此接收方能夠用這種方法來進行計算。有了上面兩個數值,咱們就能夠知道發送的數據是從第幾個字節開始,長度是多少了。
經過這些信息,接收方還可以檢查收到的網絡包有沒有遺漏。例如,假設上次接收到第1460字節,那麼接下來若是收到序號爲1461的包,說明中間沒有遺漏;但若是收到的包序號爲2921,那就說明中間有包遺漏了。若是確認沒有遺漏,接收方會將到目前爲止接收到的數據長度加起來,計算出一共已經收到了多少個字節,而後將這個數值寫入TCP頭部的ACK號中發送給發送方。
然而,圖中的例子和實際狀況仍是有些出入的。在實際的通訊中,序號並非從1開始的,而是須要用隨機數計算出一個初始值,所以須要在開始收發數據以前將初始值告知通訊對象。你們應該還記得在咱們剛纔講過的鏈接過程當中,有一個將SYN控制位設爲1併發送給服務器的操做,就是在這一步將序號的初始值告知對方的。實際上,在將SYN設爲1的同時,還須要同時設置序號字段的值,而這裏的值就表明序號的初始值。
TCP數據收發是雙向的,在客戶端向服務器發送數據的同時,服務器也會向客戶端發送數據,過程也相似。
咱們來總結一下實際的工做過程,以下圖。首先,客戶端在鏈接時須要計算出與從客戶端到服務器方向通訊相關的序號初始值,並將這個值發送給服務器。接下來,服務器會經過這個初始值計算出ACK號並返回給客戶端。同時,服務器也須要計算出與從服務器到客戶端方向通訊相關的序號初始值,並將這個值發送給客戶端。接下來像剛纔同樣,客戶端也須要根據服務器發來的初始值計算出ACK號並返回給服務器。到這裏,序號和ACK號都已經準備完成了,接下來就能夠進入數據收發階段了。數據收發操做自己是能夠雙向同時進行的,但Web中是先由客戶端向服務器發送請求,序號也會跟隨數據一塊兒發送。而後,服務器收到數據後再返回ACK號。從服務器向客戶端發送數據的過程則正好相反。
TCP採用這樣的方式確認對方是否收到了數據,在獲得對方確認以前,發送過的包都會保存在發送緩衝區中。若是對方沒有返回某些包對應的ACK號,那麼就從新發送這些包。
前面說的只是一些基本原理,實際上網絡的錯誤檢測和補償機制很是複雜。下面來講幾個關鍵的點,首先是返回ACK號的等待時間(這個等待時間叫超時時間)。
當網絡傳輸繁忙時就會發生擁塞,ACK號的返回會變慢,這時咱們就必須將等待時間設置得稍微長一點,不然可能會發生已經重傳了包以後,前面的ACK號才姍姍來遲的狀況。這樣的重傳是多餘的,並且對於原本就很擁塞的網絡來講無疑是雪上加霜。那麼等待時間是否是越長越好呢?也不是。若是等待時間過長,那麼包的重傳就會出現很大的延遲,也會致使網絡速度變慢。
等待時間須要設爲一個合適的值,不能太長也不能過短。根據服務器物理距離的遠近,ACK號的返回時間也會產生很大的波動,並且咱們還必須考慮到擁塞帶來的影響。正由於波動如此之大,因此將等待時間設置爲一個固定值並非一個好辦法。所以,TCP採用了動態調整等待時間的方法,這個等待時間是根據ACK號返回所需的時間來判斷的。具體來講,TCP會在發送數據的過程當中持續測量ACK號的返回時間,若是ACK號返回變慢,則相應延長等待時間;相對地,若是ACK號立刻就能返回,則相應縮短等待時間。
每發送一個包就等待一個ACK號的方式是最簡單也最容易理解的,但在等待ACK號的這段時間中,若是什麼都不作那實在太浪費了。爲了減小這樣的浪費,TCP採用滑動窗口方式來管理數據發送和ACK號的操做。所謂滑動窗口,就是在發送一個包以後,不等待ACK號返回,而是直接發送後續的一系列包。
雖然這樣作可以減小等待ACK號時的時間浪費,但有一些問題須要注意。在一來一回方式中,接收方完成接收操做後返回ACK號,而後發送方收到ACK號以後才繼續發送下一個包,所以不會出現發送的包太多接收方處理不過來的狀況。但若是不等返回ACK號就連續發送包,就有可能會出現發送包的頻率超過接收方處理能力的狀況。
當接收方的TCP模塊收到包後,會先將數據存放到接收緩衝區中。而後,接收方須要計算ACK號,將數據塊組裝起來還原成本來的數據並傳遞給應用程序,若是這些操做還沒完成下一個包就到了也不用擔憂,由於下一個包也會被暫存在接收緩衝區中。若是數據到達的速率比處理這些數據並傳遞給應用程序的速率還要快,那麼接收緩衝區中的數據就會越堆越多,最後就會溢出。緩衝區溢出以後,後面的數據就進不來了,所以接收方就收不到後面的包了,也就意味着超出了接收方處理能力。咱們能夠經過下面的方法來避免這種狀況的發生。首先,接收方須要告訴發送方本身最多能接收多少數據,而後發送方根據這個值對數據發送操做進行控制,這就是滑動窗口方式的基本思路。
在這張圖中,接收方將數據暫存到接收緩衝區中並執行接收操做。當接收操做完成後,接收緩衝區中的空間會被釋放出來,也就能夠接收更多的數據了,這時接收方會經過TCP頭部中的窗口字段將本身能接收的數據量告知發送方。這樣一來,發送方就不會發送過多的數據,致使超出接收方的處理能力了。
接收方可以接收的最大數據量稱爲窗口大小,它是TCP調優參數中很是有名的一個。
要提升收發數據的效率,還須要考慮另外一個問題,那就是返回ACK號和更新窗口的時機。若是假定這兩個參數是相互獨立的,分別用兩個單獨的包來發送,結果會如何?
首先,何時須要更新窗口大小?當收到的數據剛剛開始填入緩衝區時,其實不必每次都向發送方更新窗口大小,由於只要發送方在每次發送數據時減掉已發送的數據長度就能夠自行計算出當前窗口的剩餘長度。
所以,更新窗口大小的時機應該是接收方從緩衝區中取出數據傳遞給應用程序的時候。這個操做是接收方應用程序發出請求時纔會進行的,而發送方不知道何時會進行這樣的操做,所以當接收方將數據傳遞給應用程序,致使接收緩衝區剩餘容量增長時,就須要告知發送方,這就是更新窗口大小的時機。
那麼ACK號又是什麼狀況?當接收方收到數據時,若是確認內容沒有問題,就應該向發送方返回ACK號。
若是將前面兩個因素結合起來看,每收到一個包,就須要向發送方分別發送ACK號和窗口更新這兩個單獨的包。這樣一來,接收方發給發送方的包就太多了,致使網絡效率降低。
所以,接收方在發送ACK號和窗口更新時,並不會立刻把包發送出去,而是會等待一段時間,在這個過程當中頗有可能會出現其餘的通知操做,這樣就能夠把兩種通知合併在一個包裏面發送了。舉個例子,在等待發送ACK號的時候正好須要更新窗口,這時就能夠把ACK號和窗口更新放在一個包裏發送。當須要連續發送多個ACK號時,也能夠只發送最後一個ACK號,從而減小包的數量。
瀏覽器發送HTTP請求消息後,接下來還須要等待Web服務器返回響應消息。對於響應消息,瀏覽器須要進行接收操做,這一操做也須要協議棧的參與。
協議棧接收數據的具體操做過程能夠簡單總結以下:首先,協議棧會檢查收到的數據塊和TCP頭部的內容,判斷是否有數據丟失,若是沒有問題則返回ACK號。而後,協議棧將數據塊暫存到接收緩衝區中,並將數據塊按順序鏈接起來還原出原始的數據,最後將數據交給應用程序。將數據交給應用程序以後,協議棧還須要找到合適的時機向發送方發送窗口更新。
毫無疑問,收發數據結束的時間點應該是應用程序判斷全部數據都已經發送完畢的時候。這時,數據發送完畢的一方會發起斷開過程,但不一樣的應用程序會選擇不一樣的斷開時機。以Web爲例,在HTTP1.0時代,服務器一方會在發送完響應後發起斷開過程。
不管哪一種狀況,完成數據發送的一方會發起斷開過程,這裏咱們以服務器一方發起斷開過程爲例來進行講解。首先,服務器一方的應用程序會調用Socket庫的close程序。而後,服務器的協議棧會生成包含斷開信息的TCP頭部,具體來講就是將控制位中的FIN比特設爲1。接下來,協議棧會委託IP模塊向客戶端發送數據。同時,服務器的套接字中也會記錄下斷開操做的相關信息。
當收到服務器發來的FIN爲1的TCP頭部時,客戶端的協議棧會將本身的套接字標記爲進入斷開操做狀態。而後,爲了告知服務器已收到FIN爲1的包,客戶端會向服務器返回一個ACK號。過了一下子,應用程序就會調用read來讀取數據。這時,協議棧會告知應用程序(瀏覽器)來自服務器的數據已經所有收到了。
所以,客戶端應用程序會調用close來結束數據收發操做,這時客戶端的協議棧也會和服務器同樣,生成一個FIN比特爲1的TCP包,而後委託IP模塊發送給服務器。一段時間以後,服務器就會返回ACK號。到這裏,客戶端和服務器的通訊就所有結束了。
和服務器的通訊結束以後,用來通訊的套接字也就不會再使用了,這時咱們就能夠刪除這個套接字了。不過,套接字並不會當即被刪除,而是會等待一段時間以後再被刪除。
等待這段時間是爲了防止誤操做,引起誤操做的緣由有不少,下面來舉一個最容易理解的例子。在HTTP 1.1中,客戶端先發起斷開,服務器返回ACK號;而後服務器也發送FIN請求斷開,客戶端返回ACK號。
若是最後客戶端返回的ACK號丟失了,結果會如何呢?這時,服務器沒有接收到ACK號,可能會重發一次FIN。若是這時客戶端的套接字已經刪除了,會發生什麼呢?套接字被刪除,那麼套接字中保存的控制信息也就跟着消失了,套接字對應的端口號就會被釋放出來。這時,若是別的應用程序要建立套接字,新套接字碰巧又被分配了同一個端口號,而服務器重發的FIN正好到達,會怎麼樣呢?原本這個FIN是要發給剛剛刪除的那個套接字的,但新套接字具備相同的端口號,因而這個FIN就會錯誤地跑到新套接字裏面,新套接字就開始執行斷開操做了。之因此不立刻刪除套接字,就是爲了防止這樣的誤操做。
至於具體等待多長時間,這和包重傳的操做方式有關。協議中對於這個等待時間沒有明確的規定,通常來講會等待幾分鐘以後再刪除套接字。
到這裏,用TCP協議收發應用程序數據的操做就所有結束了。一圖勝千言,下圖描述了整個過程。
TCP模塊在執行鏈接、收發、斷開等各階段操做時,都須要委託IP模塊將數據封裝成包發送給通訊對象。咱們就來討論一下IP模塊是如何將包發送給對方的。
包是由頭部和數據兩部分構成的。頭部包含目的地址等控制信息,頭部後面就是委託方要發送給對方的數據。
發送方的網絡設備會負責建立包,建立包的過程就是生成含有正確控制信息的頭部,而後再附加上要發送的數據。接下來,包會被髮往最近的網絡轉發設備。當到達最近的轉發設備以後,轉發設備會根據頭部中的信息判斷接下來應該發往哪裏。
這個過程須要用到一張表,這張表裏面記錄了每個地址對應的發送方向,也就是按照頭部裏記錄的目的地址在表裏進行查詢,並根據查到的信息判斷接下來應該發往哪一個方向。接下來,包在向目的地移動的過程當中,又會到達下一個轉發設備,而後又會按照一樣的方式被髮往下一個轉發設備。就這樣,通過多個轉發設備的接力以後,包最終就會到達接收方的網絡設備。
網絡中有路由器和集線器兩種不一樣的轉發設備,它們在傳輸網絡包時有着各自的分工。
集線器是按照以太網規則傳輸包的設備,而路由器是按照IP規則傳輸包的設備,所以咱們也能夠做以下理解。
TCP/IP包包含兩個頭部:MAC頭部(用於以太網協議)和IP頭部(用於IP協議)。這兩個頭部分別具備不一樣的做用。發送方將包的目的地,也就是要訪問的服務器的IP地址寫入IP頭部中。IP協議就能夠根據這一地址查找包的傳輸方向,從而找到下一個路由器的位置。接下來,IP協議會委託以太網協議將包傳輸過去。這時,IP協議會查找下一個路由器的以太網地址(MAC地址),並將這個地址寫入MAC頭部中。這樣一來,以太網協議就知道要將這個包發到哪個路由器上了。
網絡包會經過路由器到達下一個路由器。這個過程不斷重複,最終網絡包就會被送到目的地,當目的地設備成功接收以後,網絡包的傳輸過程就結束了。
包收發操做的起點是TCP模塊委託IP模塊發送包的操做。這個委託的過程就是TCP模塊在數據塊的前面加上TCP頭部,而後整個傳遞給IP模塊,這部分就是網絡包的內容。與此同時,TCP模塊還須要指定通訊對象的IP地址。
收到委託後,IP模塊會將包的內容看成一整塊數據,在前面加上包含控制信息的頭部。剛纔咱們講過,IP模塊會添加IP頭部和MAC頭部這兩種頭部。IP頭部中包含IP協議規定的、根據IP地址將包發往目的地所需的控制信息;MAC頭部包含經過以太網的局域網將包傳輸至最近的路由器所需的控制信息。
接下來,封裝好的包會被交給網絡硬件,例如以太網、無線局域網等。傳遞給網卡的網絡包是由一連串0和1組成的數字信息,網卡會將這些數字信息轉換爲電信號或光信號,並經過網線(或光纖)發送出去,而後這些信號就會到達集線器、路由器等轉發設備,再由轉發設備一步一步地送達接收方。
包送達對方以後,對方會做出響應。返回的包也會經過轉發設備發送回來,而後咱們須要接收這個包。接收的過程和發送的過程是相反的。
IP模塊接受TCP模塊的委託負責包的收發工做,它會生成IP頭部並附加在TCP頭部前面。IP頭部包含的內容如圖所示,其中最重要的內容就是IP地址,它表示這個包應該發到哪裏去。這個地址是由TCP模塊告知的,而TCP又是在執行鏈接操做時從應用程序那裏得到這個地址的,所以這個地址的最初來源就是應用程序。
IP頭部中還須要填寫發送方的IP地址。IP地址實際上並非分配給計算機的,而是分配給網卡的,所以當計算機上存在多塊網卡時,每一塊網卡都會有本身的IP地址。不少服務器上都會安裝多塊網卡,這時一臺計算機就有多個IP地址,在填寫發送方IP地址時就須要判斷到底應該填寫哪一個地址。這個判斷至關於在多塊網卡中判斷應該使用哪一塊網卡來發送這個包,也就至關於判斷應該把包發往哪一個路由器,所以只要肯定了目標路由器,也就肯定了應該使用哪塊網卡,也就肯定了發送方的IP地址。
那麼,咱們應該如何判斷應該把包交給哪塊網卡呢?其實和路由器使用IP表判斷下一個路由器位置的操做是同樣的。
這個「IP表」叫做路由表,這裏先簡單介紹一下。如圖所示,咱們能夠經過route print命令來顯示路由表。首先,咱們對套接字中記錄的目的地IP地址與路由表左側的Network Destination欄進行比較,找到對應的一行。例如,TCP模塊告知的目標IP地址爲192.168.1.21,那麼就對應圖中的第6行,由於它和192.168.1的部分相匹配。
找到相應的條目以後,接下來看從右邊數第2列和第3列的內容。右起第2列,也就是Interface列,表示網卡等網絡接口,這些網絡接口能夠將包發送給通訊對象。右起第3列,即Gateway列表示下一個路由器的IP地址,將包發給這個IP地址,該地址對應的路由器就會將包轉發到目標地址。路由表的第1行中,目標地址和子網掩碼都是0.0.0.0,這表示默認網關,若是其餘全部條目都沒法匹配,就會自動匹配這一行。
這樣一來,咱們就能夠判斷出應該使用哪塊網卡來發送包了,而後就能夠在IP頭部的發送方IP地址中填上這塊網卡對應的IP地址。
IP模塊在生成IP頭部以後,會在它前面再加上MAC頭部。MAC頭部是以太網使用的頭部,它包含了接收方和發送方的MAC地址等信息。
在生成MAC頭部時,只要設置上圖中的3個字段就能夠了。首先是「以太類型」,這裏填寫表示IP協議的值0800。接下來是發送方MAC地址,這裏填寫網卡自己的MAC地址。MAC地址是在網卡生產時寫入ROM裏的,只要將這個值讀取出來寫入MAC頭部就能夠了。
前面這些還比較簡單,而接收方MAC地址就有點複雜了,由於咱們還須要執行根據IP地址查詢MAC地址的操做。
這裏咱們須要使用ARP,它其實很是簡單。在以太網中,有一種叫做廣播的方法,能夠把包發給鏈接在同一以太網中的全部設備。ARP就是利用廣播對全部設備提問:「××這個IP地址是誰的?請把你的MAC地址告訴我。」而後就會有人回答:「這個IP地址是個人,個人MAC地址是××××。」
若是對方和本身處於同一個子網中,那麼經過上面的操做就能夠獲得對方的MAC地址。而後,咱們將這個MAC地址寫入MAC頭部,MAC頭部就完成了。
不過,若是每次發送包都要這樣查詢一次,網絡中就會增長不少ARP包,所以咱們會將查詢結果放到一塊叫做ARP緩存的內存空間中留着之後用。也就是說,在發送包時,先查詢一下ARP緩存,若是其中已經保存了對方的MAC地址,就直接使用ARP緩存中的地址,而當ARP緩存中不存在對方MAC地址時,則發送ARP查詢。顯示ARP緩存的方法以下。
有了ARP緩存,咱們能夠減小ARP包的數量,但若是老是使用ARP緩存中保存的地址也會產生問題。例如當IP地址發生變化時,ARP緩存的內容就會和現實發生差別。爲了防止這種問題的發生,ARP緩存中的值在通過一段時間後會被刪除,通常這個時間在幾分鐘左右。這個刪除的操做很是簡單粗暴,無論ARP緩存中的內容是否有效,只要通過幾分鐘就所有刪掉。當地址從ARP緩存中刪除後,只要從新執行一次ARP查詢就能夠再次得到地址了。
上面這個策略可以在幾分鐘後消除緩存和現實的差別,但IP地址剛剛發生改變的時候,ARP緩存中依然會保留舊的地址,這時就會發生通訊的異常。
完成IP模塊的工做以後,下面就該輪到網卡了,不過在此以前,咱們先來了解一些以太網的基本知識。
以太網是一種爲多臺計算機可以彼此自由和廉價地相互通訊而設計的通訊技術,它的原型如圖(a)所示。這種網絡的本質其實就是一根網線,圖上還有一種叫做收發器的小設備,它的功能只是將不一樣網線之間的信號鏈接起來而已。所以,當一臺計算機發送信號時,信號就會經過網線流過整個網絡,最終到達全部的設備。
不過,咱們沒法判斷一個信號究竟是發給誰的,所以須要在信號的開頭加上接收者的信息,也就是地址。這樣一來就可以判斷信號的接收者了,與接收者地址匹配的設備就接收這個包,其餘的設備則丟棄這個包,這樣咱們的包就送到指定的目的地了。爲了控制這一操做,咱們就須要使用MAC頭部。經過MAC頭部中的接收方MAC地址,就可以知道包是發給誰的;而經過發送方MAC地址,就可以知道包是誰發出的;此外,經過以太類型就能夠判斷包裏面裝了什麼類型的內容。
這個原型後來變成了圖(b)中的結構。這個結構是將主幹網線替換成了一箇中繼式集線器,將收發器網線替換成了雙絞線。不過,雖然網絡的結構有所變化,但信號會發送給全部設備這一基本性質並無改變。後來,圖(c)這樣的使用交換式集線器的結構普及開來,如今咱們說的以太網指的都是這樣的結構。這個結構看上去和(b)很像,但其實裏面有一個重要的變化,即信號會發送給全部設備這一性質變了,如今信號只會流到根據MAC地址指定的設備,而不會到達其餘設備了。
下面來看看以太網的包收發操做。IP生成的網絡包只是存放在內存中的一串數字信息,咱們須要將數字信息轉換爲電或光信號,才能在網線上傳輸。負責執行這一操做的是網卡,要控制網卡還須要網卡驅動程序。下面是一張網卡主要構成要素的概念圖。
網卡並非通上電以後就能夠立刻開始工做的,而是和其餘硬件同樣,都須要進行初始化。也就是說,打開計算機啓動操做系統的時候,網卡驅動程序會對硬件進行初始化操做,而後硬件才進入可使用的狀態。其中包括在MAC模塊中設置MAC地址。
網卡驅動從IP模塊獲取包以後,會將其複製到網卡內的緩衝區中,而後向MAC模塊發送發送包的命令。接下來就輪到MAC模塊進行工做了。
首先,MAC模塊會將包從緩衝區中取出,並在開頭加上報頭和起始幀分界符,在末尾加上用於檢測錯誤的幀校驗序列。
報頭是一串像10101010…這樣1和0交替出現的比特序列,長度爲56比特,它的做用是肯定包的讀取時機,SFD則是用來肯定幀的起始位置。
末尾的FCS(幀校驗序列)用來檢查包傳輸過程當中因噪聲致使的波形紊亂、數據錯誤,它是一串32比特的序列,是經過一個公式對包中從頭至尾的全部內容進行計算而得出來的。在包傳輸過程當中,若是受到噪聲的干擾而致使其中的數據發生了變化,那麼接收方計算出的FCS和發送方計算出的FCS就會不一樣,這樣咱們就能夠判斷出數據有沒有錯誤。
加上報頭、起始幀分界符和FCS以後,咱們就能夠將包經過網線發送出去了。發送信號的操做分爲兩種,一種是使用集線器的半雙工模式,另外一種是使用交換機的全雙工模式。半雙工模式須要考慮信號的碰撞問題,不過如今所使用的基本上都是全雙工模式。
首先,MAC模塊從報頭開始將數字信息按每一個比特轉換成電信號,而後由PHY,或者叫MAU的信號收發模塊發送出去。將數字信息轉換爲電信號的速率就是網絡的傳輸速率,例如每秒將10Mbit的數字信息轉換爲電信號發送出去,則速率就是10 Mbit/s。
接下來,PHY(MAU)模塊會將信號轉換爲可在網線上傳輸的格式,並經過網線發送出去。以太網規格中對不一樣的網線類型和速率以及其對應的信號格式進行了規定,但MAC模塊並不關心這些區別,而是將可轉換爲任意格式的通用信號發送給PHY(MAU)模塊,而後PHY(MAU)模塊再將其轉換爲可在網線上傳輸的格式。你們能夠認爲PHY(MAU)模塊的功能就是對MAC模塊產生的信號進行格式轉換。
咱們繼續看看接收網絡包時的操做過程。
信號的開頭是報頭,經過報頭的波形同步時鐘,而後遇到起始幀分界符時開始將後面的信號轉換成數字信息。首先,PHY (MAU)模塊會將信號轉換成通用格式併發送給MAC模塊,MAC模塊再從頭開始將信號轉換爲數字信息,並存放到緩衝區中。當到達信號的末尾時,還須要檢查FCS。若是計算得出的FCS和包末尾的FCS不一致,這個包就會被看成錯誤包而被丟棄。
若是FCS校驗沒有問題,接下來就要看一下MAC頭部中接收方MAC地址與網卡在初始化時分配給本身的MAC地址是否一致。若是不一致就直接丟棄,一致的話則將包放入緩衝區中。到這裏,MAC模塊的工做就完成了,接下來網卡會經過中斷機制通知計算機收到了一個包。
服務器返回的包的以太類型應該是0800,所以網卡驅動會將其交給TCP/IP協議棧來進行處理。接下來就輪到IP模塊先開始工做了,第一步是檢查IP頭部,確認格式是否正確。若是格式沒有問題,下一步就是查看接收方IP地址。
若是接收方IP地址不是本身的地址,那必定是發生了什麼錯誤。客戶端計算機不負責對包進行轉發,所以不該該收到不是發給本身的包。當發生這樣的錯誤時,IP模塊會經過ICMP消息將錯誤告知發送方。ICMP規定了各類類型的消息,以下圖所示。當咱們遇到這個錯誤時,IP模塊會經過Destination unreachable消息通知對方。
若是接收方IP地址正確,則這個包會被接收下來,這時還須要完成另外一項工做。IP協議有一個叫做分片的功能,若是接收到的包是通過分片的,那麼IP模塊會將它們還原成原始的包。
到這裏,IP模塊的工做就結束了,接下來包會被交給TCP模塊。TCP模塊會根據IP頭部中的接收方和發送方IP地址,以及TCP頭部中的接收方和發送方端口號來查找對應的套接字。找到對應的套接字以後,就能夠根據套接字中記錄的通訊狀態,執行相應的操做了。
大多數的應用程序都像以前介紹的同樣使用TCP協議來收發數據,但固然也有例外。有些應用程序不使用TCP協議,而是使用UDP協議來收發數據。向DNS服務器查詢IP地址的時候咱們用的也是UDP協議。下面就簡單介紹一下UDP協議。
TCP的工做方式十分複雜,爲何要設計得如此複雜呢?由於咱們須要將數據高效且可靠地發送給對方。爲了實現可靠性,咱們就須要確認對方是否收到了咱們發送的數據,若是沒有還須要再發一遍。
要實現上面的要求,最簡單的方法是數據所有發送完畢以後讓接收方返回一個接收確認。這樣一來,若是沒收到直接所有從新發送一遍就行了,根本不用像TCP同樣要管理髮送和確認的進度。可是,若是漏掉了一個包就要所有重發一遍,怎麼看都很低效。爲了實現高效的傳輸,咱們要避免重發已經送達的包,而是隻重發那些出錯的或者未送達的包。
不過,在某種狀況下,即使沒有TCP這樣複雜的機制,咱們也可以高效地重發數據,這種狀況就是數據很短,用一個包就能裝得下。若是隻有一個包,就不用考慮哪一個包未送達了,由於所有重發也只不過是重發一個包而已。此外,咱們發送了數據,對方通常都會給出回覆,只要將回復的數據看成接收確認就好了,也不須要專門的接收確認包了。
這種狀況就適合使用UDP。像DNS查詢等交換控制信息的操做基本上均可以在一個包的大小範圍內解決,這種場景中就能夠用UDP來代替TCP。UDP沒有TCP的接收確認、窗口等機制,所以在收發數據以前也不須要交換控制信息,也就是說不須要創建和斷開鏈接的步驟,只要在從應用程序獲取的數據前面加上UDP頭部,而後交給IP進行發送就能夠了。
接收也很簡單,只要根據IP頭部中的接收方和發送方IP地址,以及UDP頭部中的接收方和發送方端口號,找到相應的套接字並將數據交給相應的應用程序就能夠了。除此以外,UDP協議沒有其餘功能了,遇到錯誤或者丟包也一律無論。由於UDP只負責單純地發送包而已,並不像TCP同樣會對包的送達狀態進行監控,因此協議棧也不知道有沒有發生錯誤。但這樣並不會引起什麼問題,所以出錯時就收不到來自對方的回覆,應用程序會注意到這個問題,並從新發送一遍數據。
還有另外一個場景會使用UDP,就是發送音頻和視頻數據的時候。音頻和視頻數據必須在規定的時間內送達,一旦送達晚了,就會錯過播放時機,致使聲音和圖像卡頓。
若是像TCP同樣經過接收確認響應來檢查錯誤並重發,重發的過程須要消耗必定的時間,所以重發的數據極可能已經錯過了播放的時機。一旦錯過播放時機,重發數據也是沒有用的,由於聲音和圖像已經卡頓了,這是沒法挽回的。
此外,音頻和視頻數據中缺乏了某些包並不會產生嚴重的問題,只是會產生一些失真或者卡頓而已,通常都是能夠接受的。在這些無需重發數據,或者是重發了也沒什麼意義的狀況下,使用UDP發送數據的效率會更高。