兩萬字長文50+張趣圖帶你領悟網路程式設計的內功心法

itzhai發表於2020-08-03

前言

我大學是學網路工程專業,也就是那種拉網線,面向網線程式設計的。依稀記得學習計算機網路這門課程的時候搭建的IT宅 itzhai.com個人網站。

算一下,學這門課程也已經快十年了。

某一天,偶然又看到了這本書:

image-20200726213509847

翻了下,發現裡面的內容竟然還是毫不過時,真的是越底層的知識越有價值呀。我擦了擦書面的灰塵,決定要為它寫點什麼,於是又從書架上找了相關的書籍:

image-20200726211636509

來回翻閱和梳理總結,逐漸輸出了這篇文章,獻給對網路不太熟悉,又想快速從入門到熟練的朋友們。

相信大家拿到Socket API,就可以很快寫好程式碼,收發訊息,傳送檔案什麼的,可是底層究竟發生了什麼?TCP、UDP、HTTP是什麼關係、為啥要有WebSocket程式設計。我們從TCP/IP協議棧以及一根網線說起,逐步揭開面向網線程式設計內功心法的面紗。

最後,在這裡解答一個問題:有人問我為什麼要寫公眾號技術文章呢?工作越久,發現身邊比自己年紀小的人越多,我也時常在想,那些同齡人或者比我大的人都去哪裡了,也許有些人忙於家庭生活不亦樂乎,有些人因為公司上市拿到可觀的收入轉行了,也許有人在大公司做起了管理工作,開始走管理路線,帶領團隊創造新的產品。我寫公眾號的原因之一,也就是想告訴大家,我一直在做技術,一個堅持寫程式碼的大齡技術人,並且希望能夠結實更多志同道合的技術人。沒錯,在說你們呢,不要求三連,這篇文章對你感興趣就點個在看唄。Thanks♪(・ω・)ノ

本文的各種電腦、伺服器、路由器小圖示都是我一筆一筆畫的用心只做了儘量美觀有趣好理解的配圖,旨在希望能夠有助於大家理解文章內容,真心希望產品經理也可以看懂。

1、網路分層

1.1、OSI七層模型

為了制定一個統一的計算機網路體系,國際標準化組織ISO提出了一個試圖使各種計算機可以在世界範圍內互聯成網的標準框架:OSI/RM(Open System Interconnection Reference Model 開放系統互連基本參考模型),該模型如下:

image-20200708085822940

媒介層:第一到第三層稱為媒體層,它們主要與硬體相關,例如路由,交換和電纜規格;

主機層:第四到第七層稱為主機層,它們是實現網路服務相關的軟體。

大致介紹一下各層(注意:看看就好,這不是重點,重點是後面的TCP/IP協議):

  • 物理層:物理層負責在裝置和物理傳輸介質直接傳輸和接收非結構化原始資料。這一層中,把數字位轉換為電,無線電或光訊號。可以發現這一層往往跟各種材質啊訊號呀什麼的打交道,所以稱為物理層;
  • 資料鏈路層:在通過物理層連線的兩個節點之間進行傳輸資料幀,檢測並且糾正物理層中可能發生的錯誤。它定義了在兩個物理連線的裝置之間建立和終止連線的協議,還定義了他們之間的流控制協議;
  • 網路層:構建和管理多節點網路,包括定址、路由、流量控制。網路層是一種可以連線許多節點的介質,每個節點都在其上有一個地址,通過目標地址就可以在節點之間傳輸資料到目標地址。網路層訊息傳輸不一定要保證可靠,網路層系可以可以提供可靠的訊息傳遞,但不必這樣做;
  • 傳輸層:傳輸層提供了將可變長度資料序列從源傳輸到目標主機的功能和過程方法,同時又保持了服務功能的質量。一些協議是面向狀態和麵向連線的,這意味著傳輸層可以進行分段傳輸、支援失敗重傳;
  • 會話層:控制計算機之間的連線,負責建立、管理和終止本地和遠端引用程式之間的連線,提供全雙工、半雙工或者單工操作;
  • 表示層:網路服務和應用程式之間的資料轉換,如字元編碼、資料壓縮、加密解密;
  • 應用層:最接近終端使用者的OSI層,該層直接與實現通訊元件的軟體應用程式進行互動。

可以發現,這個模型還真有點複雜。但很可惜,這個模型似乎不怎麼流行,原因如下:

  • OSI專家缺乏實際經驗,缺少商業驅動;
  • OSI協議實現過於複雜,執行效率低;
  • 標準制定週期長,市場已被其他標準佔據;
  • 層次劃分不太合理,部分功能在多個分層中出現。

在1980年代末和1990年代初的一段時間內,工程師、組織和國家對哪種標準(OSI模型或Internet協議套件)將更能塑造最佳和最強大的計算機網路存在爭端,導致兩極分化。儘管OSI在1980年代後期開發了其網路標準,後來更多的供應商網路上更多采用的卻是TCP / IP標準,最終TCP/IP成為了實時的國際標準。

1.2、TCP/IP四層模型

下面我們把TCP/IP模型和OSI模型放一起對比下:

image-20200708234353206

可以發現,TCP/IP體系少了表示層和會話層,資料鏈路層和物理層用鏈路層取代。

  • 應用層:最高層,應用層的任務是通過應用程式間互動來實現特定網路應用。主要負責把應用程式中的使用者資料傳達給另一臺主機或同一主機上的其他應用程式。這是所有應用程式協議的執行層,如SMTP、FTP、SSH、HTTP等;
  • 傳輸層:負責向兩個主機中的程式之間的通訊提供通用的資料傳輸服務。UDP是基本的傳輸層協議,提供了不可靠的無連線資料包傳輸服務;
  • 網路層:負責為分組交換網上的不同主機提供通訊服務。該層定義了定址和路由功能,主要協議是IP協議(Internet Protocol),它定義了IP地址,它在路由中的功能是將資料包傳輸到充當IP路由器的下一個主機,該主機更接近最終資料目的地;
  • 鏈路層:也稱為資料鏈路層或者網路介面層,通常包括作業系統中裝置驅動程式和計算機對應的網路介面卡。它們負責處理與傳輸媒介的物理介面細節。

而後面我們除了講到各種協議之外,還會順便提及一些底層硬體,為了更好的進行闡述,我們將把鏈路層再分為物理層和資料鏈路層,採用以下這種五層模型:

image-20200711112221679

根據執行模式分為以下兩種:

  • 執行於使用者程式:應用層,關注應用程式的細節,不關注底層網路通訊細節;
  • 執行於核心:傳輸層、網路層、鏈路層,在核心中執行,主要處理所有的通訊細節。

介紹了這麼多概念,是不是比較難懂呢,沒關係,我們列一下每一層主要的協議,接下來我們會詳細的講解各種協議的原理。

1.3、分層模型如何工作

這裡我們通過一個FTP客戶端的通訊流程,來說明下兩臺主機是如何工作在TCP/IP分層模型上的:

image-20200711135012000

如上圖,A主機的FTP客戶端要與B主機的FTP伺服器進行互動。

我們對裝置做一下劃分:

  • 端系統(End system):A主機和B主機
  • 中間系統(Intermediate system):路由器

其中用到的協議也做一下劃分:

  • 端對端協議(End-to-end):包括應用層和傳輸層,端系統直接直接進行互動;
  • 逐跳(Hop-by-hop)協議:網路層,需要經過端系統中所有的中間系統。

對於應用層來說,他們好像是直接與端系統進行互動的,應用層根本不知道底層通訊用了多少個路由器,是在乙太網上還是在令牌環網上的。

什麼是三層裝置,二層裝置?

如上圖,路由器工作在網路層,屬於第三層,所以經常有人稱他為三層裝置;而後面我們會講到交換機,他是工作在第二層-資料鏈路層,所以也成為二層裝置。

1.3.1、為什麼要分層?

正如上面的例子,分層之後是的頂層遮蔽了底層的物理和通訊細節。底層的通訊原理是在較低的協議層中實現的,因此每個程式都將隱藏大多數通訊細節。以此類推,在傳輸層,通訊表現為主機到主機,而無需瞭解應用程式資料結構和連線的路由器。而在網際網路絡層,則在每個路由器上遍歷各個網路邊界。

就像我們搞軟體開發劃分層次一樣,分層之後,提高了軟體的複用度,封裝每層細節,使用者只需要關注使用的API就可以了,不用關注實現細節。你不要告訴我你的一個功能涉及的一萬行程式碼是寫在一個函式裡面的,那太可怕了。

想要了解底層細節的人,就只能拆開TCP/IP協議潘多拉之盒,逐個協議去了解了,這也是本文後邊會繼續探討的內容。高度的封裝,使得頂層開發人員能更快速的通過API開發應用程式。作為一個API工程師,你知道怎麼呼叫API傳送HTTP請求就夠了,但是作為一個有追求的工程師,你瞭解了這些細節之後,就能夠勝任程式調優以及更加底層的開發工作了。這也是為什麼我堅持寫IT宅 itzhai.com部落格的原因:我想探索技術的本質,而不是生活在API構造的童話世界裡面,這樣即使童話世界謊言被拆穿的那天,也不至於失掉技術的信仰,因為我仍然有能力構建一個新的童話世界。

image-20200711120503245

接下來我們看看資料包從在傳輸過程中是如何封裝的。

1.3.2、資料包的封裝和分用

為了演示,我們需要構建一個區域網(LAN)。

假設我們現在直接通過兩個網線把兩臺電腦連起來進行通訊,需要做哪些工作呢。多虧我大學學的是網路工程,也是拉過網線的,所以多少還知道一點:

  • 準備一根網線,兩個水晶頭;
  • 水晶頭要做交叉線,採用1-3,2-6交叉接法,保證兩個水晶頭之間能夠正常收發訊號;
  • 把兩個水晶頭分別插在兩臺主機的電腦上;
  • 給兩臺電腦配置IP、子網掩碼和閘道器,必須要配置到同一個網路中。

這樣我們就構建好了一個最簡單的區域網了。

image-20200726223529480

1.3.2.1、封裝

所謂封裝,就是每一層都會根據用到的協議,把資料封裝成最終的一個資料單元(不同分層有不同的叫法,參考上圖最左邊每層的描述),每一層拿到的上一層的內容,把上一層封裝好的內容作為當前層的資料,然後加上自己的協議頭或者尾,接著執行該層協議的相關處理邏輯。

有沒有發現,這有點像裝飾者模式,每一層拿到上一層的內容之後都新增額外的處理邏輯,但是不改變上層傳過來的內容。

image-20200726223316970

如上圖,左邊部分為封裝的過程:

  • A主機請求B主機的HTTP服務,應用層使用HTTP協議對請求內容進行封裝,加上HTTP請求頭,然後傳給下一層;
  • 傳輸層拿到HTTP資料,使用TCP協議進行處理,加上自己的TCP頭,封裝成資料段,通過TCP協議傳輸給下一層。這一層通過TCP協議保證了可靠的傳輸
  • 網路層拿到TCP傳輸段之後,使用IP協議進行處理,加上IP頭,封裝成包進一步傳給下一層。這個IP決定了什麼路由或者主機需要接收處理這個包;
  • 資料鏈路層拿到網路層的包之後,進一步封裝成資料幀,最終通過資料鏈路層進行傳送處理,最終資料幀通過物理層傳輸給接收端。

1.3.2.2、分用

所謂分用,指的是主機或者中間裝置接收到一個物理層傳輸過來的資料幀時,資料開始從協議棧中由底向上升,逐層處理,每層去掉對應的協議的報文首部。每層協議盒都要檢查報文首部的協議標識進行對應的協議處理。

image-20200726223316970

在上圖中,右邊部分為分用的過程:

  • 主機B接收到物理層傳過來的資料幀之後,首先從首部找到MAC地址,判斷是否傳送給自己的,如果不是則進行丟棄;
  • 如果傳送包是自己的,則從資料幀確定資料協議型別,再傳給對應的協議模組,如IP、ARP等;
  • IP模組接收到資料後獲取IP首部判斷首部接收的IPIP地址匹配,如果匹配則根據首部協議型別轉發給對應的模組,如TCP、UDP等;
  • 傳到TCP模組之後,首先TCP模組會計算校驗和,判斷資料的完整性,然後處理資料包的順序接收相關邏輯;最後檢查埠號,確定具體應該要轉發給應用層的哪個應用程式。
  • 應用層接收到資料之後,解析資料進行展示,這裡是HTTP資料包,所以按照HTTP協議的約定進行解析展示。

可以發現,以上流程中,大家感知最深刻的就是傳輸層的TCP或者UDP協議,以及應用層的HTTP協議了,因為做網站開發,或者網路通訊程式設計經常會用到它們的API。

上面介紹的並不是很詳細,不過沒關係,後面我們會把主要的協議都拿出來詳細的講解。

當然,正常的網站請求中,中間肯定會涉及到很多路由器,交換機,光纖等底層的物理裝置,中間會產生很多的逐跳(Hop-by-hop),每個中間系統都會對資料幀進行分用和封裝的過程。

1.4、TCP/IP協議簇

TCP/IP協議簇內容非常多,這裡列出的是本文可能會介紹到的相關協議,以及他們之間的互動關係:

image-20200726204748249

2、物理層

我們的資料幀究竟是怎麼傳給不同的主機呢。前面我們瞭解到每一個上層都依賴於下層的API,而物理層是最底層的了,它是真的要把資料傳出去了。而資料最終都會變為0和1,物理層依賴於各種不同硬體技術,通過網路的電子傳輸技術,把0和1在傳輸介質中進行傳輸。

2.1、通訊系統的模型

下我我們舉一個最簡單的例子來說明通訊系統的模型[1]

很久以前,有些同學家裡都是用的電話線進行上網的,這種網路傳輸模型類似如下這樣:

image-20200712230429639

如上圖,主要包括源系統,傳輸系統,目的系統,可以抽象為下半部分的模型:

  • 源點:源點產生要傳輸的資料;
  • 傳送器:源點產生的資料經過傳送器編碼之後進行傳輸;
  • 傳輸系統:傳輸系統可能是簡單的傳輸線,也可能是複雜的網路系統;
  • 接收器:接收傳輸系統的訊號,轉換為能夠被目的裝置處理的資訊;
  • 終點:從接收器獲取傳送過來的數字位元流,最終輸出資訊。

2.2、物理層解決什麼

傳輸媒介的種類非常多:雙絞線、對稱電纜、同軸電纜、光纜、無線通道等,導致物理層的協議種類較多。

物理層的主要作用是遮蔽掉這些傳輸媒介和通訊手段的差異,使物理層上面的資料鏈路層感覺不到這些差異。為此,物理層需要處理以下事情

  • 規定介面所用接線器的形狀和尺寸,引腳數目和排列,固定和鎖定裝置等;
  • 規定介面電纜各條線上的電壓範圍;
  • 規定某一電平電壓的意義;
  • 規定不同功能的各種可能事件出現順序。

2.3、物理層也出面試題?

最後我列幾個物理層常見的面試題,一般的開發人員都是工作在傳輸層以上,所以考一些TCP,UDP,HTTP,HTTPS等協議我覺得更貼近開發人員真實的工作場景。當然,如果是通訊領域的工程師,物理層都是家常便飯,這些可是通訊的基礎知識。即使知識應用開發工程師,瞭解這些也不會吃虧,說不定哪天親戚還需要叫你幫忙拉網線呢。

下面是幾個常見的物理層面試題:

有哪些通訊互動方式?單工、半雙工通訊、全雙工通訊?

單工通訊,又稱為單向通訊,只有一個方向的通訊,如無線電廣播,電視廣播;

image-20200712122203747

半雙工通訊,又稱為雙向交替通訊,雙方都可以收發資訊,只能交替進行;

全雙工通訊,又稱為雙向同時通訊,雙方可以同時傳送和接收資料。

image-20200712122339778

為了提高通道利用率,有哪些通道複用技術?

所謂通道複用技術,指的是大家共享一個通道進行通訊,在接收端在使用分用器,把合起來傳輸的資訊分別送到相應的終點;

分頻多工

使用者在分配到一定的頻帶後,通訊過程中使用都佔用這個頻帶;

image-20200712123343946

分時多工

將時間劃分為一段段等長分時多工幀,每一個分時多工的使用者週期性的佔用幀位;

image-20200712123728888

統計分時多工

分時多工,如果使用者沒有任何資料要傳輸,也會週期性的給他分配時隙,這就導致了通道利用率不高。

為此出現了統計分時多工。

統計分時多工使用STDM幀來傳送複用的資料,把所有使用者資料按時間順序組成STDM幀,放入一個佇列中,依次傳送出去,這樣就能夠更合理的共享通道。STDM幀中的資料需要新增使用者地址首部資訊,以便能夠正確的分發給目標使用者:

image-20200712130521935

這裡的集中器也叫智慧複用器。

除了以上三種,還有波長分波多工和碼分複用,感興趣的朋友可以自行搜尋資料瞭解,這裡就不繼續展開來講了。

物理層要解決什麼問題?

這個問題上一小節已經回答了。

2.4、物理層裝置之集線器

如果我們只是想用幾臺電腦搭建一個區域網,那麼可以通過集線器(Hub)進行搭建,這個硬體工作在物理層,會把自己收到的位元組都複製到其他埠,如下圖:

image-20200712162725654

如上圖,其中一臺電腦傳送資訊之後,Hub以廣播的方式發給其他三臺機器,但是究竟哪臺電腦才會把訊息接收下來呢?這裡我們就要講到資料鏈路層了,在這一層判斷資料包是不是自己的。

3、資料鏈路層

3.1、資料幀格式

我們首先來看看資料鏈路層的傳輸資料幀的格式。

所有的乙太網(802.3)幀都基於一個共同的格式。在原有規範的基礎上,幀格式已被改進以支援額外功能。

當前乙太網的幀格式[2]如下:

image-20200715085955337

前導:用在傳送方和接收方之間同步時鐘和bit流;

SFD:幀開始界定符,只有一個byte,內容固定為:10101011 (0xAB);

DST:目標MAC地址;

SRC:源MAC地址;

長度或型別:0800時,表示IP資料包,0806表示ARP請求/應答,0835表示RARP請求/應答;

FCS:幀檢驗序列,用於資料幀的差錯檢測;

這個包應該發給誰?

判斷是否應該接受這個包,就是通過幀的MAC地址進行判斷的。

這是一個實體地址,叫做鏈路層地址,因為鏈路層主要解決媒體接入控制問題,所以稱為MAC地址(Media Access Control Address)。實際上,MAC地址就是介面卡地址或介面卡識別符號,當介面卡插入到某臺計算機之後,介面卡上的識別符號就成為這臺計算機的MAC地址了。

怎麼校驗包是否出現錯誤

FCS是幀校驗序列,也就是迴圈冗餘檢測,收到資料包之後,會通過一個檢驗計算規則,把計算結果與FCS欄位匹配,如果匹配補上,則幀可能在傳輸過程中受損,通常會丟棄該幀。

3.2、ARP: 如何獲取目標機器的MAC地址?

我們知道,在資料鏈路層,是通過MAC地址判斷某一個接收到的包是不是要進一步處理的。但是如果我們不知道對方的MAC地址的時候,如何傳送資料鏈路層的幀呢?這就需要用到資料鏈路層的ARP協議了。

ARP協議:ARP為IP地址到硬體地址之間提供了動態對映,我們通過ARP可以把32位的Internet地址轉換為48位的MAC地址。另外,我們可以使用RARP,把48位的MAC地址轉換為32位的Internet地址。

image-20200713220351269

另外,為了保證ARP的高效執行,ARP會維護每個主機和路由器上的ARP快取,把Internet地址和MAC地址的對映關係儲存起來,快取正常到期時間是20分鐘。

下面是這個過程的演示,其中ARP資料幀只把關鍵資訊描述出來了,想要了解完整的幀格式可以用參考 TCP/IP協議詳解卷1[3]

image-20200713231114251

如上圖:

主機A想知道192.168.1.4這個IP地址的MAC地址是什麼,發現本地快取中找不到,於是廣播了一個ARP請求,主機B和主機D收到之後,發現自己不是192.168.1.4於是忽略這個訊息,主機C發現自己就是192.168.1.4,於是響應了一個ARP資料幀。最終主機A收到主機C響應的資料幀,拿到了MAC地址,並把IP地址和MAC地址對映關係儲存下來。

3.3、鏈路層裝置之交換機

3.3.1、為什麼需要交換機?

前面我們用了集線器元件網路,這個時候所有訊息都會廣播到其他埠,可以發現集線器轉發了很多不必要的訊息,能不能只發給需要的埠呢?這個時候就需要用到交換機了。

當一臺電腦A向交換機傳送資料時,交換機會把電腦A的IP和MAC地址記住,儲存到一個轉發表中,如果轉發表中暫時找不到目標IP地址的MAC地址,那麼首先還是會廣播訊息,最終轉發表會記錄所有請求過交換機的電腦IP和MAC。當然,轉發表也是有過期時間的。

image-20200713232323689

如上圖,看到交換機的奸笑沒有,與集線器不同,交換機是有靈魂的的,你告訴他你的身份證號和住址了,他就會偷偷記下來。

3.3.2、為什麼有了IP地址,還需要有MAC地址?

IP地址是工作在網路層的,後面會講到;

MAC地址是工作在資料鏈路層的,也就是交換機這一層,交換機之間的主機進行通訊,都是用的MAC地址,但是一旦走出了區域網,我們就得用大家都公認的IP地址了。

MAC地址就好像是我們的身份證,IP就像是我們的住址,可以根據住址寄送快遞,但是不能根據身份證號碼寄快遞,別人不知道怎麼走呢。

一個區域網,用身份證沒有問題呀,因為要找某個人,ARP會大喊一聲名字,那個人就會告訴你他的身份證號碼了,這個時候直接以身份證作為標識傳訊息,別人聽到不是自己的身份證就不管了。最終交換機這個小管家記住了所有人的名字跟身份證號碼,就會使用悄悄話的方式傳達訊息了。這也就是用到了交換機的轉發表。

3.3.3、交換機拓撲環路問題

假設我現在拉網線,搞了一個這樣的拓撲結構:

image-20200714225908316

如上圖,主機準備傳送一個訊息出去,結果交換機B收到後,複製資料幀,傳送給了交換機A、C、D,此時交換機B認為主機是在左邊。但是不妙的事情發生了,交換機D收到訊息後,由於轉發表還是空的,又是也複製資料幀,轉發到了交換機A、B、C,這個時候交換機B發現怎麼主機的資料又從右邊傳過來了,這些徹底暈了,不知道主機究竟在哪裡。就這樣資料一致在這個網路裡面打轉,這就拓撲環路導致的問題。

生成樹協議

為了解決以上問題,於是 有了生成樹協議(Spanning Tree Protocol,STP)。

STP通過在每個交換機禁用某些埠工作,來避免拓撲環路,保證不會出現重複路徑。

STP會找到拓撲結構的一個生成樹,通過生成樹避免環路。生成樹的形成和維護有多個網橋完成,在每個網橋上執行一個分散式演算法。

以上拓撲,結構,最終應用了生成樹,禁用一些埠之後,可能會是這樣:

image-20200714225935282

這樣,訊息就不可能再傳回左邊的主機了,從而避免了拓撲環路導致的問題。

建立生成樹:

網橋會傳送一種稱為網橋協議資料單元(BPDU)的幀來輔助形成和維護生成樹。

STP首先會嘗試選舉根網橋,根網橋是在網路中識別符號最小的網橋(也就是說優先順序與MAC地址結合),網橋初始化的時候,假設自己是最小的網橋,然後用自己的網橋ID作為根ID欄位的值傳送配置BPDU訊息,如果發現ID更小的網橋,那麼會停止傳送自己的幀,並基於接收到的ID更小的幀構造下一步傳送的BPDU訊息。發出根ID更小的BPDU埠被標記為根埠,剩餘埠被設定為阻塞或者轉發狀態。

3.4、VLAN

3、網路層

網路層,Internet layer,最熟知的就是IP協議了(Internet Protocol)。

前面我們將的資料鏈路層,其實只能在區域網內進行通訊,因為都是通過MAC地址進行傳達資訊的,要想跨區域網,那麼就得用到IP地址了,這就是網路層要做的事情了。

首先我們來介紹下網路的一個協議:ICMP協議。

3.1、ICMP協議

IP協議本身不支援發現發往目的地地址失敗的IP資料包,也沒有提供直接的方式獲取診斷資訊,比如在傳送途中,經過了哪些路由器,以及往返時間。

為此,就有了ICMP協議Internet Control Message ProtocolICMP)專門來負責這些事情。

ICMP並不為IP網路提供可靠性,它只是用於反饋各種故障和配置資訊。丟包不會觸發ICMP。

ICMP是RFC 792中定義的Internet協議套件的一部分。ICMP訊息通常用於診斷網路或探測網路目的,或者是為了響應IP操作中的錯誤而生成(如RFC 1122中所指定),ICMP錯誤響應給原始資料包的源IP地址。

但是黑客經常用ICMP來做壞事,於是網路管理員可能會用防火牆阻止掉ICMP報文,這樣的話,很多ping、traceroute之類的診斷程式就無法正常工作了。

3.1.1、格式

ICMP報文是在IP資料包內部傳輸的,格式如下:

image-20200715084813447

而ICMP報文的格式如下:

image-20200715085522615

其中:

  • 型別有15個不同的值,描述特定型別的ICMP報文;
  • 某些ICMP報文還是用程式碼欄位的值來進一步描述不同的條件;
  • 校驗和欄位用於ICMP報文的差錯檢查。

以下是常見的差錯報文型別:

型別 程式碼 描述 查詢 差錯
0 0 回顯應答(ping應答)
3 目標不可達
0 網路不可達
1 主機不可達
2 協議不可達
3 埠不可達
4 需要進行分片但設定了不分片位元
5 源站選路失敗
6 目的網路不認識
7 目的主機不認識
8 源主機被隔離(作廢不用)
9 目的網路被強制禁止
10 目的主機被強制禁止
11 由於服務型別 T O S , 網 絡 不 可 達
12 由於服務型別 T O S , 主 機 不 可 達
13 由於過濾,通訊被強制禁止
14 主機越權
15 優先權中止生效
4 0 源端抑制
5 重定向
0 對網路重定向
1 對網路重定向
2 對服務型別和網路重定向
3 對服務型別和主機重定向
8 0 回顯請求(ping)
9 0 路由器通告
10 0 路由器請求
11 超時
0 傳輸期間生存時間為0(Traceroute)
1 在資料包組裝期間生存時間為0
12 引數問題
0 壞的 I P 首部(包括各種差錯)
1 缺少必需的選項
13 0 時間戳請求
14 0 時間戳應答
15 0 資訊請求
16 0 資訊應答
17 0 地址掩碼請求
18 0 地址掩碼應答

其中,最常用的型別是8:回顯請求(ping),以及0:回顯應答(ping應答)。

3.1.2、查詢報文

查詢報文是有關資訊採集和配置的ICMP報文。

我們經常用到的ping程式就用到了ICMP查詢報文。

ping程式

ping程式會傳送一份ICMP回顯請求給主機,並等待返回ICMP回顯應答。

ping程式ping不通了,就不能訪問對應的主機了嗎?

我們知道,網路管理員可能會用防火牆阻止掉ICMP報文的,這樣我們可能就ping不通了,但是主機的可達性不能只取決於IP層是否可達,還與埠號和協議有關,而ping是執行在網路層的,用於測試網路連線狀態和資訊包傳送接收狀況,即使ping不通,我們也可能用telnet遠端登入到主機的其他埠,如25號埠。

ping程式用到了回顯請求和回顯應答報文,報文格式如下:

image-20200715233138672

Unix系統實現ping程式時,把ICMP報文的識別符號設定為程式ID,在程式內,序號從0開始,每傳送一次新的 回顯請求就加1,這樣就可以同時執行多個ping程式了。

ping程式的埠號是什麼?

埠號是傳輸層的東西,ping程式是使用ICMP協議,直接跳過了傳輸層,所以呢,ping程式是沒有所謂的埠號的。

我們傳送一個ping請求,資料在協議棧中的處理流程如下:

image-20200716085927626

  1. A主機的ping應用程式向伺服器發起回顯請求,說了一句:hi
  2. 直接傳輸到網路層的ICMP協議,進行ICMP資料封裝:
    1. image-20200716085942614
    2. 8表示回顯請求,112是發起請求的程式號,1表示請求序號
  3. IP協議拿到資料後進一步加上IP頭,加上自己的IP和目標IP,傳輸給資料鏈路層;
  4. 資料鏈路層拿到IP資料包,準備封裝成幀,這個時候會去尋找目標IP的MAC地址,如果在A主機的ARP對映表找到了IP的MAC地址,那就直接拿來用了,否則會發起一個ARP廣播請求,獲取到MAC地址。至於跨閘道器這種ping,會多了轉發的功能更,後面會進行介紹。最終資料鏈路層封裝成資料幀,從網路介面發出去;
  5. 伺服器拿到資料幀之後,拿到MAC頭,判斷MAC地址是自己的,就基於拿到Frame data,按首部協議傳給對應的模組,即IP模組;
  6. IP模組拿到資料,判斷到IP跟自己對上了,與是繼續拿到IP data,傳輸給ICMP協議,ICMP協議收到訊息,準備應答:
    1. image-20200716085953616
    2. 0表示回顯應答
  7. 然後按照同樣的流程,把資料包傳送回A主機。

可以發現,ping程式是直接用到了網路層的ICMP協議,不經過傳輸層

是什麼原因導致ping失敗了?

ping失敗的原因有很多:

  • 可能是輸錯了IP;
  • 可能是網路配置不正確,如錯誤的子網掩碼;
  • 可能有防火牆軟體組織了ping;
  • 可能是硬體故障,如損壞了的乙太網介面卡,電纜,路由器,集線器等。

如果叫你自己實現一個ping程式,你會怎麼做呢?

提示:為了能處理ICMP網路報文,我們需要用到原始套接字(SOCK_RAW),而不是SOCK_STREAM或者SOCK_DGRAM套接字。

更多提示:Homework 6: A raw socket ping tool[4],思路都在這裡了,大家動手做一做,然後就可以有直接操作網路層的工作經驗了。?

3.1.3、差錯報文

差錯報文是有關IP資料包傳遞的ICMP報文。要是傳送IP資料包中途產生了異常,那麼就會響應ICMP差錯報文。

但是不是所有情況都會響應ICMP差錯報文,如以下場景:

  • ICMP差錯報文不會產生另一個ICMP差錯報文;
  • 目的地址是廣播地址或者多播地址的IP資料包不會產生差錯報文;
  • 作為鏈路層廣播的資料包不會產生差錯報文;
  • 源地址不是單個主機(源地址為零地址、環回地址、廣播地址或者多波地址)的資料包不會產生差錯報文;

為什麼要這些規則呢?假如允許ICMP差錯報文對廣播分組響應,那麼就會導致廣播風暴了。

下面我們舉一個ICMP差錯報文的例子來說明下。

目標不可達

上面的表格我們瞭解到,如果型別為三則表示目標不可達,而根據具體的程式碼可以進一步劃分:

型別 程式碼 描述 查詢 差錯
3 目標不可達
0 網路不可達
1 主機不可達
2 協議不可達
3 埠不可達
4 需要進行分片但設定了不分片位元

下面我們看一個埠不可達的例子來演示下ICMP差錯報文附加的資訊。

ICMP埠不可達案例

這裡我們演示通過tftp訪問一個不存在的埠號,檢視其返回的ICMP響應差錯報文。tftp應用在傳輸層是通過UDP來進行傳輸資料的

下面我們tftp請求之前先開啟tcpdump抓包:

sudo tcpdump -i en0 -nn host 我的IP and 目標IP

然後執行tftp命令:

tftp
tftp> connect 目標IP 8090
tftp> get test.foo
Transfer timed out.

可以發現,在等待了大約25秒之後,最終輸出:Transfer timed out.

觀察tcpdump抓包日誌:

16:51:16.739632 IP 我的IP.64517 > 目標IP.8090: UDP, length 20
16:51:16.768590 IP 目標IP > 我的IP: ICMP host 193.112.43.165 udp port 8090 unreachable, length 56
16:51:21.743056 IP 我的IP.64517 > 目標IP.8090: UDP, length 20
16:51:21.751958 IP 目標IP > 我的IP: ICMP host 193.112.43.165 udp port 8090 unreachable, length 56
16:51:26.748387 IP 我的IP.64517 > 目標IP.8090: UDP, length 20
16:51:26.757631 IP 目標IP > 我的IP: ICMP host 193.112.43.165 udp port 8090 unreachable, length 56
16:51:31.752851 IP 我的IP.64517 > 目標IP.8090: UDP, length 20
16:51:31.794217 IP 目標IP > 我的IP: ICMP host 193.112.43.165 udp port 8090 unreachable, length 56
16:51:36.755172 IP 我的IP.64517 > 目標IP.8090: UDP, length 20
16:51:36.774400 IP 目標IP > 我的IP: ICMP host 193.112.43.165 udp port 8090 unreachable, length 56

可以發現這裡執行了五次UDP請求,每次請求都響應了一個ICMP包,為udp port 8090 unreachable埠不可達,產生了ICMP不可達報文,該報文一般格式如下:

image-20200718171533801

為什麼需要返回IP首部:因為IP首部包含了協議欄位,使得ICMP知道如何解釋後面的8個位元組;

為什麼需要原始IP資料包中資料的前8個位元組:因為這裡麵包含了源埠和目的埠。

不過看起來我的電腦好像忽略了ICMP報文,還是繼續重試了4次。

注意:ICMP報文是在主機之間交換的,網路層的協議,不需要埠號,而以上20個位元組的UDP資料包是包含了源埠號和目標埠號資訊的。

為什麼TFTP客戶程式會繼續重發呢?

因為網路程式設計中,BSD系統不把從socket接收到的ICMP報文中的UDP資料通知使用者程式,除非該程式以及傳送了一個connect命令給該介面。標準的BSDTFTP客戶程式並不傳送connect命令,所以它永遠也不會受到ICMP差錯報文的通知。

traceroute程式

traceroute工具用於確定從傳送者到目的地路徑上的路由器。

traceroute主要是通過故意設定特殊的TTL,來達到追蹤目的地路徑上的路由器的功能。

TTL執行原理

TTL:是 Time To Live的縮寫,該欄位指定IP包被路由器丟棄之前允許通過的最大網段數量。每經過一個路由器,TTL就會減一,然後再把IP包轉發出去,如果TTL減到0了,路由器就會丟棄收到的TTL=0的IP包,並向IP包的傳送者傳送一個ICMP差錯報文,型別為11,程式碼為0:傳輸期間生存時間為0。

第一輪,traceroute設定TTL值為1,那麼遇到第一個路由就返回ICMP容錯報文了,下一輪,TTL設定為2...這樣依次增加。最終就把整個鏈路的路由器都試出來了。

當然,有點路由器不會回整個ICMP,這也是為什麼你去traceroute一個公網地址,看不到中間路由的原因。

除此之外,traceroute也可以通過不設定分片,來確定傳輸鏈路的MTU(Maximum Transmission Unit, 最大傳輸單元):首先傳送一個分組的長度正好與出口MTU相等,如果中間遇到窄點的關口,就被卡主了,這個時候會接收到一個ICMP差錯報文,然後調小分組長度重試...

3.2、閘道器與路由器

在講資料鏈路層的時候,我們用一個交換機,就構建了一個區域網。但是現在我們區域網裡面的一臺機器,想要訪問另一個區域網的機器,怎麼辦呢。這就是本節討論的內容。

我們必須先連線下IP協議。

3.2.1、IP協議

IP是TCP/IP協議簇中最核心的協議,所有TCP、UDP、ICMP等資料都已IP資料包格式進行傳輸。

3.2.1.1、IP協議特點

  • IP協議是不可靠的傳輸協議,上一級我們講到到了ICMP協議,每當傳輸出現異常,IP層都會丟棄資料包,並且可能會響應一個ICMP差錯訊息給傳送端,而任何要求的可靠性必須由上層如TCP協議來提供
  • IP協議是無連線的,也就是說IP不維護任何關於後續資料包的狀態資訊,每個資料包相互獨立。具體表現在:可以不按傳送順序接收,不用維護連線狀態,免去了維護複製的連結狀態資訊(後面講傳輸層的TCP協議的時候會介紹到)。

3.2.1.2、IP資料包格式

下面是IP資料包的格式:

image-20200719221922958

  • 版本:協議版本號,指明IPv4還是IPv6;
  • 頭部長度:最長60個位元組;
  • 服務型別:包含3bit優先權子欄位(已被忽略),4bit TOS子欄位(分別代表最小時延、最大吞吐量、最高可靠性和最小費用)和1bit未用位但必須置0;
  • 總長度:指的是整個IP資料包的長度,單位位元組;
  • 識別符號:唯一地標識主機傳送的每一份資料包,通常每傳送一個資料包就+1;
  • 標誌:主要用於IP分片;
  • 分片偏移:主要用於IP分片;
  • 生存期:設定資料包可以經過最多的路由器數;
  • 協議:主要表明IP資料是什麼協議,用於對資料包進行分用;
  • 頭部校驗和:校驗資料包是否正確;
  • 源IP地址:傳送IP資料包的IP地址;
  • 目的IP地址:IP資料包目的IP地址;
  • 選項:可選資料;
  • IP資料:具體的IP資料;

3.2.2、路由器

路由器一般充當一個閘道器,屬於三層裝置。會把MAC和IP頭取下來根據內容進行處理。路由器有五個網口,分別可以連線5個區域網,每個網口和區域網的IP地址相同的網段,每個網口都是對應的區域網的閘道器。

5個網口中一般包含一個外網網口,外網網口用於連線到WAN上。

路由器除了有交換機的功能外,更擁有路由表作為傳送資料包時的依據,在有多種選擇的路徑中選擇最佳的路徑。

一層裝置、二層裝置、三層裝置分別有什麼區別?

路由器是屬於OSI第三層的產品,交換機是OSI第二層的產品。

第二層的產品功能在於,將網路上各個電腦的MAC地址記在MAC地址表中,當區域網中的電腦要經過交換機去交換傳遞資料時,就查詢交換機上的MAC地址表中的資訊,將資料包傳送給指定的電腦,而不會像第一層的產品(如集線器)每臺在網路中的電腦都傳送。

而路由器除了有交換機的功能外,更擁有路由表作為傳送資料包時的依據,在有多種選擇的路徑中選擇最佳的路徑。此外,並可以連接兩個以上不同網段的網路,而交換機只能連線兩個。路由表儲存了(向前往)某一網路的最佳路徑、該路徑的“路由度量值”以及下一個(跳路由器)

閘道器地址一般是網段的第一個或者第二個,如192.168.23.0/24這個網段,閘道器地址可能是192.168.23.1/24或者192.168.23.2/24。

在不同的區域網中,私有IP地址是會重複的,而我們要訪問公網的時候,一定要分配一個共有IP地址,所以,我們在訪問公網的時候,需要路由器幫忙把私有IP變為共有IP,這種叫做NAT閘道器,普通內網之間的通訊用到的稱為轉發閘道器。

我們先來看看轉發閘道器

3.2.2.1、轉發閘道器

假設主機A和主機B屬於同一個內網,他們通過兩個路由器連線起來,如下圖:

image-20200719171542654

主機A要訪問主機B,流程如下:

  • 主機A發現要訪問的主機B不是在同一個網段,準備先找到閘道器,把訊息發給閘道器,閘道器地址是192.168.1.1,主機A通過ARP獲取到了閘道器的MAC地址,然後傳送如下資料包:

    • SRC MAC: 主機A的MAC
      DST MAC: 路由器A的192.168.1.1網口的MAC
      SRC IP : 192.168.1.3
      DST IP : 192.168.3.4
      
  • 路由器A的192.168.1.1網口接收包之後,準備把包轉發出去。而路由器A中的路由表中匹配到了,要想傳送給192.168.3.4/24,需要從192.168.2.1這個網口出去,下一跳地址為192.168.2.2/24。路由器通過ARP拿到了下一跳192.168.2.2/24d的MAC地址,然後傳送如下資料包:

    • SRC MAC: 路由器A的192.168.2.1網口的MAC
      DST MAC: 路由器B的192.168.2.2網口的MAC
      SRC IP : 192.168.1.3
      DST IP : 192.168.3.4
      
  • 路由器B的192.168.2.2網口接收包之後,準備把包轉發出去。路由器B中判斷到目標IP在192.168.3.1這個網口所在的區域網,於是通過ARP拿到了192.168.3.4的MAC地址,然後傳送如下資料包:

    • SRC MAC: 路由器B的192.168.3.1網口的MAC
      DST MAC: 主機192.168.3.4網口的MAC
      SRC IP : 192.168.1.3
      DST IP : 192.168.3.4
      

最終,主機B收到資料包。

可以發現在轉發閘道器中,源IP和目的IP地址都是不會變的,因為整個內網不可能有衝突的IP

但是,假如我們要訪問外網,情況就不一樣了,最終可能會請到到另一個區域網,另一個區域網的私有IP是可能跟我們所在的區域網一樣的,為了避免衝突,於是就有了NAT閘道器。專門在把資料包傳送出去之前,把IP改為公網IP。

3.2.2.2、NAT閘道器

現在假設主機A要訪問另一個城市的主機B,這裡為了演示NAT,我們把模型簡化一下,假設路由器出去之後就是公網IP了,如下:

image-20200719184500782

假設路由器A和路由器B都直接接入了網際網路。

現在主機A想訪問主機B:

  • 由於是不同的區域網,主機A不會知道主機B的IP的,而主機B接入網際網路的之後,領取到了一個網際網路的IP,就是上圖路由器WAN口的IP:203.0.113.103,所以主機B會把這個IP作為主機B的IP,最終發出如下IP資料包:

    • SRC MAC: 主機A的MAC
      DST MAC: 路由器A的192.168.1.1網口的MAC
      SRC IP : 192.168.1.3
      DST IP : 203.0.113.103
      
  • 192.168.1.1網口接收到包之後,發現要想訪問203.0.113.103,就要從203.0.113.102這個網口出去,發給路由器B,路由器B中判斷到目標IP就是203.0.113.103這個網口,於是通過ARP拿到了203.0.113.103的MAC地址,然後傳送如下資料包:

    • SRC MAC: 路由器A的203.0.113.102網口的MAC
      DST MAC: 路由器B的203.0.113.103網口的MAC
      SRC IP : 203.0.113.102
      DST IP : 203.0.113.103
      
    • 因為訊息是要發到公網的,最終SRC IP會被NAT為公網的IP 203.0.113.102;

  • 最終路由器B接收到訊息,通過NAPT得到最終接收資料包的IP為當前區域網的192.168.1.3/24,最終把訊息轉發給了這個IP所在的主機B。

NAPT是如何把一個公網IP翻譯為區域網IP的?

傳統的NAT(traditional NAT)包括基本NAT(basic NAT)和網路地址埠轉換(Network Address Port Translation, NAPT)。基本NAT只執行IP地址的重寫,本質上是將私有地址改寫為一個公共地址,這往往取自於一個由ISP提供的地址池或共有地址範圍,這種NAT不是最流行的,因為無助於減少需要使用的IP地址數量。

比較流行的做法是使用NAPT,NAPT使用傳輸層識別符號如TCP或者UDP埠,或者ICMP查詢識別符號來確定一個特定的資料包到底和NAT內部哪臺私有主機相關聯。

如果區域網兩個埠號一樣,那麼NAPT會重寫埠號,保證不一致。如下圖,三個區域網的IP需要轉換為公網IP,由於有兩個的埠重複了,於是NAPT進行了埠重寫:

image-20200719184357681

3.3、路由策略

3.3.1、靜態路由

我們通過route命令和iproute命令都可以進行路由策略的配置和查詢。

  • 可以指明去哪個網路,走哪個網口,網口的IP是什麼;
  • 也可以建立不同的路由表,針對不同的請求來源,走不同的路由表配置;
  • 當然,也可以按照權重給下一跳地址走配置;
  • 同一個路由,也可以配多個運營商的網路,針對不同的IP,採用不同的運營商網路
  • ...

配置時非常靈活的,但是在複雜的網路環境下手動配置路由成本太大了,並且網路結構也是經常發生改版的。

所以,我們可以使用動態路由路由器,這種路由器會根據路由協議演算法生成動態路由表,動態的隨著網路執行狀況調整路由表。

3.3.2、動態路由協議

網路是複雜的,為了生成動態的路由表,需要配合特定的演算法,主流的動態路由主流有兩種演算法。

內網路由協議

基於鏈路狀態演算法實現的OSPF協議(Open Shortest Path First, 開放式最短路徑優先):主要用於資料中心內部,因此也成為內網路由協議(Interior Gateway Protocol,IGP),關鍵是找到最短的路徑。

OSPF是一種鏈路狀態路由協議。可以將其視為網路的分散式地圖。

外網路由協議

基於距離向量演算法實現的BGP協議(Border Gateway Protocol,外網路由協議):距離向量,就是每個路由器都儲存一個路由表,路由表每行儲存了下一跳的路由器,以及距離下一跳路由器的距離。也成為邊界閘道器協議。

在BGP的世界中,每個路由域都稱為自治系統或AS。BGP所做的工作通常是通過選擇遍歷最少自治系統的路由:最短的AS路徑來幫助選擇通過Internet的路徑。

我們會把重點放在傳輸層以上,所以動態路由協議這部分我們暫時不做不深入研究。

4、傳輸層

傳輸層涉及到兩個重要的協議:UDP和TCP,本節我們重點介紹這兩個協議。

4.1、UDP協議

4.1.1、UDP資料包格式

UDP基本沒幹啥事,繼承了IP包的特性:資料可能丟失,順序傳輸無法保證。UDP與後邊介紹的TCP不一樣,是無狀態的。我們來看看UDP資料包的格式:

image-20200719222327297

  • 源埠號:傳送資料包方使用的埠號,用於標識傳送程式;
  • 目的埠號:接收資料包方使用的埠號,用於標識接收程式;
  • UDP長度:UDP頭部和UDP負載資料的位元組長度;
  • UDP校驗和:UDP校驗和覆蓋UDP頭部和UDP資料和一個偽頭部(區別:IP頭部校驗和只覆蓋IP頭部),偽頭部衍生子IPv4頭部欄位的12個位元組,或者衍生子IPv6頭部欄位的一個40位元組的偽頭部;
  • 負載資料:具體的UDP資料。

可以發現,UDP與下層不同,是需要埠號的。

為什麼UDP需要埠號,TCP和UDP埠號可以相同嗎?

類似ICMP協議回顯請求的識別符號,UDP的埠用於區分是哪個程式的資料包,如果沒有埠號,那麼就不知道應該把資料包最終交給哪個程式來處理了。

TCP埠號由TCP來檢視,UDP埠號由UDP來檢視,TCP埠號和UDP埠號是相互獨立的,所以是可以相同的。每個請求都有源IP、目標IP、源埠號、目標埠、協議五個元素來標識的,每個協議的埠池是完全獨立的。

為什麼UDP的埠號最多是65535個?

在UDP/TCP協議中源埠和目的埠都只有16位,也就是說埠的取值範圍為0~65535。

4.1.2、UDP特點

UDP在IP層之上,沒有做其他的封裝,主要表現如下特點:

  • 資料可能丟失,順序傳輸無法保證;
  • 無狀態,不需要像TCP那樣要建立連線;
  • 沒有擁塞控制,來一個包就發一個。

4.1.3、UDP使用場景

基於UDP的特點,UDP主要用於以下場景:

  • 需要資源少,在網路情況比較好的內網,或者對對包不敏感的場合。如DHCP和TFTP就是基於UDP的;
  • 廣播場景,不需要一對一建立連線,如DHCP;
  • 需要時延低,允許丟包,不關注網路擁塞的場景,如視訊直播這種流媒體,實時遊戲,通訊,物聯網等領域。

4.2、TCP協議

TCP是我們平時用到最多的協議,特別是做web開發的時候,或者網際網路後端開發,真的是時時刻刻都會用到,這裡我會展開來講。《TCP/IP詳解-卷1:協議》一書中花了6章來講解TCP的各種功能,單單是從TCP/IP協議棧的名稱就可以看出,TCP協議的分量有多重了。為此,面試官張口就聊TCP咋的咋的。

img

與UDP不同,TCP做了很多功能的封裝與實現。

先來簡單介紹下TCP協議

TCP給應用程式提供給了一種與UDP完全不同的服務。

TCP是面向連線的可靠的服務面向連線指TCP的兩個應用程式必須在它們可交換資料之前,通過相互聯絡來建立一個TCP連線;

TCP提供了一種位元組流抽象概念給應用程式:TCP不會自動插入記錄標誌或者訊息邊界,這意味著TCP沒有限制應用程式的寫範圍。傳送端分兩次發10位元組和30位元組,接收端可能會以兩個20位元組的方式讀入。

我們還是先來看看TCP資料包的格式吧,這個可比UDP複雜多了,但是也是設計的恰到好處的。

4.2.1、TCP資料包格式

image-20200721222551224

如上圖,頭部深黃色部分為TCP特有的重點欄位,後面TCP相關功能基本都是靠這些特有的欄位來實現的。

  • 源埠號和目的埠號:同UDP一樣,主要用於區分資料應該轉發給哪個應用;
  • 序號:這個序號是為了解決亂序問題,32位無符號數,到達2^32-1後再重新從0開始;
  • 確認號:確認已經接收到了哪裡,該確認序號表示該確認號的傳送方期望接收的下一個序列號。該欄位只有在ACK位欄位被啟用的情況下才有效,所以也成為ACK號或者ACK段;
  • 狀態位:該狀態位會讓TCP連線雙方的狀態發生流轉,常見的狀態為,後面講建立連線和斷開連線的時候會用到:
    • ACK:回覆狀態,啟用該狀態的情況下,確認號有效,連線建立之後一般都是啟用狀態;
    • SYN:發起一個連線;
    • RST:重置連線,連線去表,經常是因為錯誤導致;
    • FIN:結束連線,表示該報文的傳送方已經結束想對方傳送資料;
    • CWR:擁塞視窗減小,傳送方降低傳送速率;
    • ECE:ECN回顯,傳送方接收到了一個更早的擁塞通告;
    • URG:緊急,表示緊急指標欄位有效,很少用到;
    • PSH:推送,表示接收方應該儘快給應用程式傳送這個資料——沒有被可靠的實現或用到;
  • 視窗大小:流量的視窗大小,用於流量控制,通訊雙方各宣告一個視窗,這個大小表明了自己當前的處理能力;
  • 校驗和:覆蓋了TCP的頭部和資料,以及偽頭部資料(與UDP使用的相似的偽頭部進行計算);
  • 緊急指標:只有在URG位啟用的時候才有效;
  • 選項:如最大段大小等其他的可選項;
  • 資料:TCP資料包的資料內容。

4.2.2、TCP特點

TCP基於以上資料包的各種欄位,實現了以下功能:

  • 資料的順序傳輸;
  • 丟包重傳,保證可靠;
  • 連線維護;
  • 流量控制,保證穩定;
  • 擁塞控制,及時調整,最大程度保證傳輸正常進行。

4.2.3、連線管理

我們首先來看看連線是如何建立的,這裡就涉及到TCP的三次握手了。

4.2.3.1、TCP三次握手

三次握手流程如下:

image-20200723224535183

可以發現,為了實現可靠連線,雙方都需要發起建立連線。具體流程如下:

  • 第一次握手:主動連線方傳送一個SYN報文段指明自己想要連線的埠號,以及客戶端訊息的初始化序列化ISN(c);
  • 第二次握手:伺服器接收到訊息後,也傳送自己的SYN報文,包含了服務端的初始化序列號ISN(s),並設定確認號ack=客戶端序列號+1;
  • 第三次握手:客戶端應答伺服器的SYN,將服務端的序列號+1作為ack返回給服務端。

總結一下:客戶端與服務端利用SYN報文交換彼此的初始化序列號。在我們熟悉的Socket程式設計中,三次握手在執行connect的時候觸發。

其中的ACK應答和遞增的序列化是可靠性的保證。

為什麼是三次握手,而不是兩次或者四次?

如果是兩次:

image-20200723001805421

客戶端請求建立連線,服務端收到了請求,並且做出了響應,很明顯,伺服器沒法知道這個響應究竟有沒有被接收,也許可能客戶端遲遲收不到SYN響應,於是結束了請求。這個時候再傳訊息網路層就會收到一個ICMP目的不可達的差錯報文。

同理:客戶端的SYN請求如果遲遲沒有伺服器的響應,那麼也會重發SYN,最終如果服務端可能收到兩個SYN,客戶端想要建立一個連線,但是伺服器收到兩個SYN之後,建立了兩個連線(當然,實際上的三次握手服務端是會判斷客戶端的請求序列號的,發現是同一個序列號,並不會建立多個連線,這也說明序列號的重要性)。

為什麼不需要四次呢?因為如果服務端和客戶端雙方都發起SYN,並且收到ACK之後,就都知道對方接受了自己的請求了,已經沒有必要再繼續確認下去了。

為什麼UDP埠號為65535個?

在TCP、UDP協議的開頭,會分別有16位來儲存源埠號和目標埠號,所以埠個數是2^16-1=65535個。

4.2.3.2、TCP四次揮手

接下來我們看看連線關閉的流程,連線的任何一方都可以發起關閉操作,此外,也支援雙方同時關閉連線。在傳統的情況下,負責發起關閉連線請求的通常是客戶端。

這個流程又被稱為四次揮手:

image-20200724000124983

  • 連線的主動關閉者傳送一個FIN段請求關閉連線,攜帶了Seq=K,指明接收方希望看到的自己的當前序列號;攜帶了ack=L,指明自己想要接受到的下一個訊息的序號。這個時候,連線主動關閉者表明了自己已經沒有資料要傳送了,但是仍然可以接受被動關閉者傳送的資料;
  • 連線的被動關閉者進行了ACK回應,ack為K+1,表明自己已經成功接收到了主動關閉者傳送的FIN。但是自己還未準備好關閉,所以主動關閉者會進入FIN_WAIT_2等待狀態;
  • 緊接著被動關閉者也傳送了一個FIN端請求關閉連線,攜帶了Seq=L。告訴主動關閉者自己也準備好了關閉;
  • 最後連線的主動關閉者接收到了對方的FIN關閉請求,也回應了一個ACK,同樣的ack=L+1,表明自己已經成功接收到了被動關閉者傳送的FIN;

可以發現,因為TCP是全雙工的,雙方都要單獨發起關閉請求,只有當連線雙方都發起FIN關閉請求操作,並且得到確認之後,才完成一個完整的關閉操作,這也是被稱為四次握手的原因。

資訊傳送期間的狀態流轉如上圖所示。其中主動關閉者在CLOSED狀態之前,有一個TIME_WAIT狀態,那麼問題來了:

為什麼要有TIME_WAIT狀態呢?

我們知道主動關閉者在應道對方的FIN請求,有可能對方是收不到的,如果收不到的情況下,那麼對方就可能認為自己的FIN請求丟失了,需要重新發起FIN請求,所以主動關閉者需要有一個足夠長的等待時間,讓對方有重試的機會

等待時間是2MSL(Maximum Segment Lifetime,報文最大生存時間),這也是報文在網路上最大的生存時間,超過了這個時間就會被丟棄。RFC 793中規定MSL為2分鐘,實際應用中常用的是30秒,1分鐘和2分鐘等。如果超過了這個時間,那麼主動關閉者就會傳送一個RST狀態位的包,表示重置連線,這個時候被動關閉者就知道對方已經關閉了連線:

image-20200724004705111

如果主動關閉者不進行等待,會出現什麼問題呢?如下:

image-20200724003739171

可以發現,由於埠複用,主動關閉者已經開啟了另一個連線,這個時候被動關閉者還在重試發起FIN請求,導致新主動關閉者新的連線收到了很多沒用的包。因為包是有序列號的,所以可以判斷到不是本次連線該接收的包。為此,我們需要讓主動關閉者進行等待,確保被動關閉者不會再發FIN請求了,再進行埠複用。

4.2.3.3、完整連線流程

完整連線流程如下:

image-20200724000001081

可以發現,每個TCP連線在正常的建立和關閉的基本開銷是7個報文段,如果只是需要交換很少量的資料時,有寫程式更願意選擇使用UDP協議。但是UDP會面臨資料丟失,擁塞管理,流量控制等問題。

4.2.4、TCP狀態機

介紹了三次握手和四次揮手,我們再看看看以下這個TCP狀態機就清晰多了。

如果沒有看過三次握手和四次揮手流程,不建議直接看這個狀態機,真的是太複雜了...不過為了方便大家能夠更直觀的看出狀態流轉,我還是繪製了下,加了一些說明:

image-20200729233117353

4.2.5、資料傳輸

4.2.5.1、如何保證可靠傳輸:ACK+序列號

假設主機A通過TCP向主機B傳送資料,當主機A的資料到達主機B時,主機B會傳送一個確認應答訊息ACK。主機A收到ACK之後,就知道自己的資料已經被對方接收了:

image-20200724081253191

如果主機一直沒有收到ACK,一定時間之後,就會重發,因此,即使主機A的資料包沒有發到主機B,或者主機B的ACK資料包丟失了,也有重傳機制,確保雙方最終可以通過重傳確保能夠正確收到訊息:

image-20200724082856702

從上圖也可以看出,主機A實際發了兩次同樣的資料給主機B,主機B可以通過序列號,判斷是重複資料,然後就丟棄了,但是還是會傳送一個ACK告訴主機A已經收到訊息。

4.2.5.2、流量控制與視窗管理

在TCP頭部中,為了實現流量控制,包括順序問題與丟包問題,我們重點關注TCP頭部的這三個欄位:序列號,序列號與確認號:
image-20200725114826908

(注意:後面部分資料傳輸圖中的傳送方統一稱為客戶端或者傳送端,接收方統一稱為服務端或者接收端,實際的資料傳輸,可以是兩臺電腦之間,或者是兩臺伺服器之間)

其中TCP頭部的視窗欄位表明自己的處理能力,代表著可用快取空間的大小,以位元組為單位。

接下來再看看滑動視窗。

TCP連線的每個端都可以收發資料,每個端的收發資料量是通過一組視窗結構來維護的。每個端都會包含一個傳送視窗結構和接收視窗結構。

傳送視窗結構

傳送視窗結構如下圖所示:

image-20200725125106588

其中:

  • SND.WND:提供視窗大小是由接收返回的ACK中的視窗大小欄位控制的;
  • SND.UNA:記錄視窗左邊界的值;
  • SND.UNA + SND.WND:記錄視窗右邊界的值;
  • SND.NXT:記錄下次傳送的資料

所謂視窗,就是左右邊界會根據情況進行調整的視窗,由主要三個動作:

  • 關閉:視窗左邊界右移,當已傳送的資料得到ACK的時候,就會進行關閉,提供視窗大小減小;
  • 開啟:視窗左邊界右移,當已確認的資料得到處理後,那麼接收端可用快取就會變大,這個時候通過開啟操作讓提供視窗大小變大;
  • 收縮:視窗右邊界左移,使得提供視窗大小減小;
接收視窗結構

接收視窗與傳送視窗結構類似,如下圖:

image-20200725143723481

從滑動視窗看如何保證可靠傳輸:順序與丟包問題

為了避免接收重複資料:接收到的資料包小於左邊界,說明是已經確認過的,將把資料包丟棄;如果接收到的資料包序列號大於右邊界,說明暫時超出了處理能力範圍,也將會被丟棄。

為了保證已確認資料包的連續性,接收到的資料包的序列號與已確認 已接受部分連續的時候,才表示真正的已確認,左邊界才可以右移。

image-20200725222605426

4.2.5.3、超時重傳機制

基於計時器的重傳超時機制(Retransmission Ttimeout, RTO)

TCP在傳送資料的時候會設定一個重傳計時器,如果計時器超時仍然沒有收到ACK確認資訊,那麼會進行重傳操作。

如下圖是超時重傳的演示說明例子:

image-20200725230608270

對於接收方來說,1,2,3都已經接收並且傳送ACK了,3的ACK丟失了。

ACK丟失的場景:過了一段時間,3的計時器發現超時了,於是會觸發超時重傳。但是這個時候接收方發現3是在已接受已確認區域,於是會丟棄3,並反饋一個ACK;

資料丟失的場景:4和5的資料傳輸丟失了,計數器發現超時,也會進行超時重傳,保證4和5可以傳給接收方,並拿到ACK反饋。

關於重傳時間間隔

ICMP埠不可達案例中,採用UDP的TFTP客戶端使用簡單且低效的超時重傳策略:設定足夠大的超時間隔,每5秒進行一次重傳;

而TCP的基於計時器的重傳策略是如果發生重試,可以有兩種處理方式:

  • 一種是基於擁塞控制機制,減小傳送視窗大小;
  • 另一種是超時時間間隔會一直加倍。

關於重傳時間

重傳時間需要講到自適應重傳演算法,一種計算重傳時間的演算法,大致流程:

TCP通過取樣RTT的時間,進行加權平均,算出一個值,最終得到一個估計的重傳時間。

初始值:原始值
測量之後:RTO = RTTs + 4*RTTd
(RTTs:加權平均值,RTTd:偏差值)

因為網路是不斷變化的,所以重傳時間也會處於變動狀態。

基於反饋資訊的快速重傳機制

快速重傳機制是這樣的:當接收方接收到一個序列號大於下一個所期望的報文段的時候,就會檢測到資料流中間丟失的間隔,然後傳送冗餘的ACK,向傳送者索要確實的間隔。當傳送者收到一定數量的冗餘的ACK(稱為重複ACK的閾值或dupthresh)之後,就不等定時器過期了,直接重傳丟失的 報文。

重複ACK的閾值通常為3,一些非標準化的實現可基於當前的失序程度動態調整。

如下例所示:傳送方的4、5、6、7都已經傳送出去了,但是接收方接收到了5、6、7,少了4,會在分別收到5、6、7的時候都發一個3的ACK,向傳送方索要下一個資料4。這樣傳送方就收到到3個3的ACK了,於是就主動發起了4的重傳,不等待重傳計時器超時了:

image-20200726104928030

帶選擇確認的重傳SACK

雖然重傳保證了資料的到達,但是重傳應該儘可能保證不重傳以正確接收到的資料,而SACK資訊能更快速的實現空缺填補並且減少不必要的重傳。

隨著選擇確認選項的標準化[RFC2018],TCP接收端可以提供SACK的功能了,通過TCP頭部的累計ACK號欄位來描述其接收到的資料。

每當快取存在失序資料時,接收端就可以生成SACK,代表著快取接收狀態地圖,這樣通過將快取的接收狀態地圖發給傳送方,傳送方就很快可以知道是什麼資料丟失併發起重傳了。

這種重傳機制下,視窗內的其他報文段也可以被接收確認,但只有在接收到等於視窗的左邊界的序列號時,視窗才會前移。這樣就減少了視窗內的不必要的重傳。

4.2.5.4、流量控制

流量控制指的是通過控制傳送方和接收方的視窗大小,以使得接收方快取中已接受的資料處理不過來時,通過減小傳送方的視窗大小,讓接收方能有足夠的時間來接收資料包;或者是接收方比較空閒時,嘗試讓傳送方調大視窗大小,以加快傳輸,合理利用空閒的網路資源。

流量控制主要是通過TCP頭的視窗大小來調節的。傳送端收到接收端的通告視窗之後,得知接收端可接收的資料量。

下面舉例來說明。

正常情況下,傳送方左邊界每關閉一格,右邊界就開啟一個,多一個可傳送的單元:

image-20200726115726307

我們知道接收端接收並確認資料之後,會放到快取中,等待應用程式處理,如果應用程式一直沒有處理,最終會導致接收端沒有更多空間來儲存到達的資料了,如果應用程式一直沒有處理資料,那麼視窗右邊界可能就不會開啟了,最終接收的視窗大小變為0:

image-20200726121531246

這個時候接收端就會傳送一個零視窗通告(TCP ZeroWindow),告知傳送端不要再傳送資料了,我已經處理不過來了,於是傳送方就暫停傳送資料了,等待接收端的視窗更新(TCP Window Update)通知:

image-20200726133315644

這樣,接收方就可以有時間來處理接收的資料了,等到有了足夠多的快取之後,於是會給傳送端傳輸一個視窗更新通知。

為了避免由於視窗更新通知ACK丟失,到時雙方陷入等待的僵局,在傳送方停止傳送資料之後,會採用一個持續計時器間歇性的查詢接收端,給接收端傳送視窗探測(TCP ZeroWindowProbe)請求,要求接收端返回TCP ZeroWindowProbeAck,看看是否視窗是否已經增加了:

image-20200726133219069

4.2.5.5、擁塞控制

前面我們講到,可以通過滑動視窗大小來控制流量,從而為接收方緩解壓力,避免不必要的丟包。

而擁塞控制,就需要用到擁塞視窗了。擁塞控制主要用於避免丟包和超時重傳。

反映網路傳輸能力的變數稱為擁塞視窗(congestion window),記為cwnd。

可以理解為滑動視窗是為接收方服務的,而擁塞視窗是為整個網路通道服務的,擁塞視窗大小又會受制於接收方滑動視窗大小,並且會因為網路原因進行調整。因為網路通道中的任何一個環節都有可能影響整體的傳輸效率。

4.2.5.5.1、傳送實際可用視窗

那麼我們傳送端實際可用視窗應該是多少了,這裡我們記實際可用視窗大小為W,那麼W為接收端通知視窗awnd和擁塞視窗cwnd的較小者:

W = min(cwnd, awnd)

假設網路沒有任何問題,並且頻寬足夠寬,資料包不會在傳輸過程遇到需要排隊等待的情況下,這種理想狀況下,也就是沒有網路延遲,接收方收到一個資料包,立刻就ACK一個,立刻空出一個可傳輸單元,傳送的實際可用視窗就是接受方的滑動視窗大小了,如下:

image-20200726144055451

理想是很美好的,但是實際網路情況是非常複雜的,TCP根本不知道里面會發生什麼情況,也許W還沒到達接收端滑動視窗大小,網路中就因為中間的瓶頸導致丟包了,那麼更加會增加重傳的頻率。所以為了能減少丟包和超時重傳,需要有一些動態傳送端視窗大小的策略。

4.2.5.5.2、傳送端視窗調整策略

雖然可以通過接收方的ACK得到對方的接收視窗大小,但是因為剛開始並不知道擁塞視窗是多少,所以只能以越來越快的速率不斷髮送資料,直到出現資料包丟失為止。

通常TCP在建立新連線的時候會執行慢啟動,直到有包丟失,然執行擁塞避免演算法進入穩定狀態。

慢啟動

初始視窗設為IW(Initial Window, IW),IW=SMSS(傳送方的最大段大小)。

先傳送初始視窗大小的資料,沒有出現丟包,並且每收到一個ACK,慢啟動演算法就會以min(N,SMSS)來增加cwnd的值。可見這是指數性的增長。

直到出現了網路擁塞,出現丟包、超時重傳,說明已經到達了慢啟動的閾值ssthresh(slow start threshold),這個時候cwnd減少一半,並作為新的ssthresh。

避免擁塞

一旦達到慢啟動的閾值之後,為了得到更多的傳輸資源而不影響其他連線的傳輸,TCP實現了擁塞避免演算法。一旦確定慢啟動閾值,TCP會進入擁塞避免階段,這個時候cwnd每次的增長值近似於成功傳輸的資料段大小。也就是說由原來慢啟動的指數增長,變為了線性增長。

4.3、Socket程式設計

4.3.1、Socket是什麼

Socket是一個抽象層,主要是把TCP/IP層複雜的操作抽象為幾個簡單的介面提供給應用層呼叫,進而實現應用程式在網路中通訊。Socket主要是端到端之間的傳輸協議(網路層之上的協議)。因為Socket是一種高層的抽象網路API,是一種端到端的通訊,只能訪問到端到端協議之上的網路層和傳輸層

image-20200726175908288

Socket起源於Unix,在Unix中,一切皆檔案,Socket也不例外,是一種開啟-讀/寫-關閉的模式實現的。在伺服器和客戶端各自維護了一個檔案。

4.3.2、基於TCP的Socket通訊互動流程

我們先來看一下基本TCP客戶/伺服器程式的套接字函式呼叫過程:

image-20200726183837690

TCP Socket的檔案結構

在核心中,Socket是一個檔案,不過Socket對應的inode不是儲存在硬碟上,而是在記憶體中,該inode指向了Socket在核心的Socket結構。核心的Socket介面主要由兩個佇列:傳送佇列,接收佇列。

核心為監聽套接字維護的兩個佇列

對於每個監聽Socket,核心都為其維護了兩個佇列:

  • 未完成佇列(incomplete connection queue):這個佇列的套接字服務端正在等待完成TCP三次握手,處於SYN_RCVD狀態;
  • 已完成連線佇列(completed connection queue):完成了三次握手的Socket連線會進入這個佇列,處於ESTABLISHED狀態。

如下圖:

image-20200726194117299

4.3.3、基於UDP的Socket通訊互動流程

UDP不需要三次握手,所以不需要listen和connect,但是互動仍然需要IP和埠號,需要bind。

UDP不用維護連線狀態,所以不需要針對每個連線建立一組Socket,只需要一個就可以了。

以下是UDP的Socket通訊互動流程圖:

image-20200726195423611

到目前為止,我們把物理層、資料鏈路層、網路層、傳輸層主要的協議和功能都介紹了一遍。基於這些底層的協議棧支撐,我們可以很快的構建出應用層的程式,接下來我們簡單講一下應用層。

5、應用層

應用層位於作業系統使用者態執行,而我們前面講到的那層是執行在作業系統核心態的:

image-20200726201637207

一般我們都是通過Socket網路API來訪問核心態的各層的協議模組。

常見的應用層協議如下:

  • HTTP:Hypertext Transfer Protocol,超文字傳輸協議,是一個基於請求與響應,無狀態的,應用層的協議,常基於TCP/IP協議傳輸資料,網際網路上應用最為廣泛的一種網路協議,所有的WWW檔案都必須遵守這個標準。設計HTTP的初衷是為了提供一種釋出和接收HTML頁面的方法;
  • HTTPS:Hypertext Transfer Protocol Secure,安全超文字傳輸協,是HTTP的擴充套件,用於在計算機網路上進行安全的通訊,並在Internet上廣泛使用;
  • 流媒體:流媒體(streaming media)是指將一連串的媒體資料壓縮後,經過網上分段傳送資料,在網上即時傳輸影音以供觀賞的一種技術與過程,此技術使得資料包得以像流水一樣傳送;如果不使用此技術,就必須在使用前下載整個媒體檔案。
  • P2P協議:Peer-to-peer,計算或聯網是一種分散式應用程式體系結構,可在對等體之間劃分任務或工作負載;對等方可以將其部分資源(例如處理能力,磁碟儲存或網路頻寬)直接提供給其他網路參與者,而無需伺服器或穩定主機的集中協調。大家經常用的迅雷下載,百度網盤下載就用到了這個協議;
  • WebSocket:WebSocket是一種計算機通訊協議,與HTTP不同,WebSocket可通過單個TCP連線提供全雙工通訊通道。WebSocket在HTML5規範中最初被稱為TCPConnection,是基於TCP的套接字API的佔位符。很多線上聊天室,辦公協同軟體都用到了WebSocket。

在應用層,大部分的開發工程師可以大展拳腳。

  • HTTP協議的實現,最經典的莫過於Tomcat伺服器了,關於具體的實現,可以參考這本書:《深入剖析Tomcat》;
  • Github上面有一個WebSocket協議的Java實現,感興趣的朋友可以研究下:Java-WebSocket[5]

到這裡,由於本文已經兩萬多字了,這裡只做一些知識的延伸,就不進一步展開來講了。

更多關於應用層的相關介紹,我會後續更新,感興趣的朋友記得關注本公眾號進一步跟進學習交流哦


這篇文章的內容就差不多介紹到這裡了,能夠閱讀到這裡的朋友真的是很有耐心,為你點個贊。

本文為arthinking基於相關技術資料和官方文件撰寫而成,確保內容的準確性,如果你發現了有何錯漏之處,煩請高抬貴手幫忙指正,萬分感激。

大家可以關注我的部落格:itzhai.com 獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網路程式設計、資料結構、資料庫、演算法、併發程式設計、分散式系統等相關內容。

如果您覺得讀完本文有所收穫的話,可以關注我的賬號,或者點贊吧,碼字不易,您的支援就是我寫作的最大動力,再次感謝!

關注我的公眾號,及時獲取最新的文章。

本文作者: arthinking

部落格連結: https://www.itzhai.com/network/comprehend-the-underlying-principles-of-network-programming.html

兩萬字長文50+張趣圖帶你領悟網路程式設計的內功心法

版權宣告: BY-NC-SA許可協議:創作不易,如需轉載,請聯絡作者,謝謝!


References

UNIX網路程式設計 卷1:套接字聯網API

TCP/IP詳解 卷1:協議(原書第2版). 機械工業出版社

謝希仁. 計算機網路(第6版). 電子工業出版社

劉超. 趣談網路協議. 極客時間


  1. 謝希仁. 計算機網路(第6版). 電子工業出版社. P39 ↩︎

  2. TCP/IP詳解 卷1:協議(原書第2版). 機械工業出版社. P57 ↩︎

  3. TCP/IP詳解 卷1:協議(原書第2版). 機械工業出版社. P116 ↩︎

  4. Homework 6: A raw socket ping tool ↩︎

  5. Java-WebSocket ↩︎

相關文章