當前位置: 妍妍網 > 碼農

你管這破玩意叫網路

2024-03-07碼農

你是 一台電腦,你的名字叫 A

很久很久之前,你不與任何其他電腦相連線,孤苦伶仃。

直到有一天,你希望與另一台電腦 B 建立通訊,於是你們各開了一個網口,用一根 網線 連線了起來。

用一根網線連線起來怎麽就能"通訊"了呢?我可以給你講 IO、講中斷、講緩沖區,但這不是研究網路時該關心的問題。

如果你糾結,要麽去研究一下作業系統是如何處理網路 IO 的,要麽去研究一下包是如何被網卡轉換成電訊號發送出去的,要麽就僅僅把它當做電腦裏有個小人在 開槍 吧~

反正,你們就是連起來了,並且可以通訊。





第一層



有一天,一個新夥伴 C 加入了,但聰明的你們很快發現,可以每個人開 兩個網口 ,用一共 三根網線 ,彼此相連。

隨著越來越多的人加入,你發現身上開的網口實在太多了,而且網線密密麻麻,混亂不堪。 (而實際上一台電腦根本開不了這麽多網口,所以這種連線只在理論上可行,所以連不上的我就用紅色虛線表示了,就是這麽嚴謹哈哈~)

於是你們發明了一個中間裝置,你們將網線都插到這個裝置上,由這個裝置做轉發,就可以彼此之間通訊了,本質上和原來一樣,只不過網口的數量和網線的數量減少了,不再那麽混亂。

你給它取名叫 集線器 ,它僅僅是無腦將電訊號 轉發到所有出口(廣播) ,不做任何處理,你覺得它是沒有智商的,因此把人家定性在了 實體層

由於轉發到了所有出口,那 BCDE 四台機器怎麽知道封包是不是發給自己的呢?

首先,你要給所有的連線到集線器的裝置,都起個名字。原來你們叫 ABCD,但現在需要一個更專業的, 全域唯一 的名字作為標識,你把這個更高端的名字稱為 MAC 地址

你的 MAC 地址是 aa-aa-aa-aa-aa-aa,你的夥伴 b 的 MAC 地址是 bb-bb-bb-bb-bb-bb,以此類推,不重復就好。

這樣,A 在發送封包給 B 時,只要在頭部拼接一個這樣結構的數據,就可以了。

B 在收到封包後,根據頭部的目標 MAC 地址資訊,判斷這個封包的確是發給自己的,於是便 收下

其他的 CDE 收到封包後, 根據頭部的目標 MAC 地址資訊,判斷這個封包並不是發給自己的,於是便 丟棄

雖然集線器使整個布局幹凈不少,但原來我只要發給電腦 B 的訊息,現在卻要發給連線到集線器中的所有電腦,這樣既不安全,又不節省網路資源。





第二層



如果把這個集線器弄得更智慧一些, 只發給目標 MAC 地址指向的那台電腦 ,就好了。

雖然只比集線器多了這一點點區別,但看起來似乎有智慧了,你把這東西叫做 交換機 。也正因為這一點點智慧,你把它放在了另一個層級, 數據鏈路層

如上圖所示,你是這樣設計的。

交換機內部維護一張 MAC 地址表 ,記錄著每一個 MAC 地址的裝置,連線在其哪一個埠上。

MAC 地址
bb-bb-bb-bb-bb-bb 1
cc-cc-cc-cc-cc-cc 3
aa-aa-aa-aa-aa-aa 4
dd-dd-dd-dd-dd-dd 5

假如你仍然要發給 B 一個封包,構造了如下的數據結構從網口出去。

到達交換機時,交換機內部透過自己維護的 MAC 地址表,發現 目標機器 B 的 MAC 地址 bb-bb-bb-bb-bb-bb 對映到了埠 1 上 ,於是把數據從 1 號埠發給了 B,完事~

你給這個透過這樣傳輸方式而組成的小範圍的網路,叫做 乙太網路

當然最開始的時候,MAC 地址表是空的,是怎麽逐步建立起來的呢?

假如在 MAC 地址表為空是,你給 B 發送了如下數據

由於這個包從埠 4 進入的交換機,所以此時交換機就可以在 MAC地址表記錄第一條數據:

MAC:aa-aa-aa-aa-aa-aa-aa
埠:4

交換機看目標 MAC 地址(bb-bb-bb-bb-bb-bb)在地址表中並沒有對映關系,於是將此包發給了 所有埠 ,也即發給了所有機器。

之後,只有機器 B 收到了確實是發給自己的包,於是做出了 響應 ,響應數據從埠 1 進入交換機,於是交換機此時在地址表中更新了第二條數據:

MAC:bb-bb-bb-bb-bb-bb
埠:1

過程如下

經過該網路中的機器不斷地通訊,交換機最終將 MAC 地址表建立完畢~

隨著機器數量越多,交換機的埠也不夠了,但聰明的你發現,只要將多個交換機連線起來,這個問題就輕而易舉搞定~

你完全不需要設計額外的東西,只需要按照之前的設計和規矩來,按照上述的接線方式即可完成所有電腦的互聯,所以交換機設計的這種規則,真的很巧妙。你想想看為什麽(比如 A 要發數據給 F)。

但是你要註意,上面那根 紅色 的線,最終在 MAC 地址表中可不是一條記錄呀,而是要把 EFGH 這四台機器與該埠(埠6)的對映全部記錄在表中。

最終, 兩個交換機將分別記錄 A ~ H 所有機器的對映記錄

左邊的交換機

MAC 地址
bb-bb-bb-bb-bb-bb 1
cc-cc-cc-cc-cc-cc 3
aa-aa-aa-aa-aa-aa 4
dd-dd-dd-dd-dd-dd 5
ee-ee-ee-ee-ee-ee
6
ff-ff-ff-ff-ff-ff
6
gg-gg-gg-gg-gg-gg
6
hh-hh-hh-hh-hh-hh
6

右邊的交換機

MAC 地址
bb-bb-bb-bb-bb-bb 1
cc-cc-cc-cc-cc-cc 1
aa-aa-aa-aa-aa-aa 1
dd-dd-dd-dd-dd-dd 1
ee-ee-ee-ee-ee-ee
2
ff-ff-ff-ff-ff-ff
3
gg-gg-gg-gg-gg-gg
4
hh-hh-hh-hh-hh-hh
6

這在只有 8 台電腦的時候還好,甚至在只有幾百台電腦的時候 ,都還好,所以這種交換機的設計方式,已經足足支撐一陣子了。

但很遺憾,人是貪婪的動物,很快,電腦的數量就發展到幾千、幾萬、幾十萬。





第三層



交換機已經無法記錄如此龐大的對映關系了。

此時你動了歪腦筋,你發現了問題的根本在於,連出去的那根 紅色的網線 ,後面不知道有多少個裝置不斷地連線進來,從而使得地址表越來越大。

那我可不可以讓那根 紅色的網線 ,接入一個 新的裝置 ,這個裝置就跟電腦一樣有自己獨立的 MAC 地址,而且同時還能幫我把封包做一次 轉發 呢?

這個裝置就是 路由器, 它的功能就是,作為一台獨立的擁有 MAC 地址的裝置,並且可以幫我把封包做一次轉發 你把它定在了 網路層。

註意,路由器的每一個埠,都有獨立的 MAC 地址

好了,現在交換機的 MAC 地址表中,只需要多出一條 MAC 地址 ABAB 與其埠的對映關系,就可以成功把封包轉交給路由器了,這條搞定。

那如何做到,把發送給 C 和 D,甚至是把發送給 DEFGH.... 的封包,統統先發送給路由器呢?

不難想到這樣一個點子,假如電腦 C 和 D 的 MAC 地址擁有共同的字首,比如分別是

C 的 MAC 地址:FFFF-FFFF-CCCC

D 的 MAC 地址: FFFF-FFFF-DDDD

那我們就可以說,將目標 MAC 地址為 FFFF-FFFF-?開頭的 ,統統先發送給路由器。

這樣是否可行呢?答案是否定的。

我們先從現實中 MAC 地址的結構入手,MAC地址也叫實體位址、硬體地址,長度為 48 位,一般這樣來表示

00-16-EA-AE-3C-40

它是由網路裝置制造商生產時燒錄在網卡的EPROM(一種快閃記憶體芯片,通常可以透過程式擦寫)。其中 前 24 位(00-16-EA)代表網路硬體制造商的編號 ,後 24 位(AE-3C-40)是該廠家自己分配的,一般表示系列號。 只要不更改自己的 MAC 地址,MAC 地址在世界是唯一的。形象地說,MAC地址就如同身份證上的身份證號碼,具有唯一性。

那如果你希望向上面那樣表示 將目標 MAC 地址為 FFFF-FFFF-?開頭的 ,統一從路由器出去發給某一群裝置(後面會提到這其實是子網路的概念),那你就需要要求某一子網路下統統買一個廠商制造的裝置,要麽你就需要要求廠商在生產網路裝置燒錄 MAC 地址時,提前按照你規劃好的子網路結構來定 MAC 地址,並且日後這個網路的結構都不能輕易改變。

這顯然是不現實的。

於是你發明了一個新的地址,給每一台機器一個 32 位的編號,如:

11000000101010000000000000000001

你覺得有些不清晰,於是把它分成四個部份,中間用點相連。

11000000.10101000.00000000.00000001

你還覺得不清晰,於是把它轉換成 10 進制。

192.168.0.1

最後你給了這個地址一個響亮的名字, IP 地址 。現在每一台電腦,同時有自己的 MAC 地址,又有自己的 IP 地址,只不過 IP 地址是 軟體層面 上的,可以隨時修改,MAC 地址一般是無法修改的。

這樣一個可以隨時修改的 IP 地址,就可以根據你規劃的網路拓撲結構,來調整了。

如上圖所示,假如我想要發送封包給 ABCD 其中一台裝置,不論哪一台,我都可以這樣描述, "將 IP 地址為 192.168.0 開頭的全部發送給到路由器,之後再怎麽轉發,交給它!" ,巧妙吧。

那交給路由器之後,路由器又是怎麽把封包準確轉發給指定裝置的呢?

別急我們慢慢來。

我們先給上面的組網方式中的每一台裝置,加上自己的 IP 地址

現在兩個裝置之間傳輸,除了加上數據鏈路層的頭部之外,還要再增加一個網路層的頭部。

假如 A 給 B 發送數據,由於它們直接連著交換機,所以 A 直接發出如下封包即可,其實網路層沒有體現出作用。

但假如 A 給 C 發送數據,A 就需要先轉交給路由器,然後再由路由器轉交給 C。由於最底層的傳輸仍然需要依賴乙太網路,所以封包是分成兩段的。

A ~ 路由器這段的包如下:

路由器到 C 這段的包如下:

好了,上面說的兩種情況(A->B,A->C),相信細心的讀者應該會有不少疑問,下面我們一個個來展開。

A 給 C 發封包,怎麽知道是否要透過路由器轉發呢?

答案:子網路

如果源 IP 與目的 IP 處於一個子網路,直接將包透過交換機發出去。

如果源 IP 與目的 IP 不處於一個子網路,就交給路由器去處理。

好,那現在只需要解決,什麽叫處於一個子網路就好了。

  • 192.168.0.1 和 192.168.0.2 處於同一個子網路

  • 192.168.0.1 和 192.168.1.1 處於不同子網路

  • 這兩個是我們人為規定的,即我們想表示,對於 192.168.0.1 來說:

    192.168.0.xxx 開頭的,就算是在一個子網路,否則就是在不同的子網路。

    那對於電腦來說,怎麽表達這個意思呢?於是人們發明了 子網路遮罩 的概念

    假如某台機器的子網路遮罩定為 255.255.255.0

    這表示,將源 IP 與目的 IP 分別同這個子網路遮罩進行 與運算 ,相等則是在一個子網路,不相等就是在不同子網路 ,就這麽簡單。

    比如

  • A電腦 :192.168.0.1 & 255.255.255.0 = 192.168.0.0

  • B電腦 :192.168.0.2 & 255.255.255.0 = 192.168.0.0

  • C電腦 :192.168.1.1 & 255.255.255.0 = 192.168.1.0

  • D電腦 :192.168.1.2 & 255.255.255.0 = 192.168.1.0

  • 那麽 A 與 B 在同一個子網路,C 與 D 在同一個子網路,但是 A 與 C 就不在同一個子網路,與 D 也不在同一個子網路,以此類推。

    所以如果 A 給 C 發訊息,A 和 C 的 IP 地址分別 & A 機器配置的子網路遮罩,發現不相等,則 A 認為 C 和自己不在同一個子網路,於是把包發給路由器,就不管了, 之後怎麽轉發,A 不關心

    A 如何知道,哪個裝置是路由器?

    答案:在 A 上要設定預設閘道器

    上一步 A 透過是否與 C 在同一個子網路內,判斷出自己應該把包發給路由器,那路由器的 IP 是多少呢?

    其實說發給路由器不準確,應該說 A 會把包發給 預設閘道器

    對 A 來說,A 只能 直接 把包發給同處於一個子網路下的某個 IP 上,所以發給路由器還是發給某個電腦,對 A 來說也不關心,只要這個裝置有個 IP 地址就行。

    所以 預設閘道器,就是 A 在自己電腦裏配置的一個 IP 地址 ,以便在發給不同子網路的機器時,發給這個 IP 地址。

    僅此而已!

    路由器如何知道C在哪裏?

    答案:路由表

    現在 A 要給 C 發封包,已經可以成功發到路由器這裏了,最後一個問題就是, 路由器怎麽知道,收到的這個封包,該從自己的哪個埠出去 ,才能直接(或間接)地最終到達目的地 C 呢。

    路由器收到的封包有目的 IP 也就是 C 的 IP 地址,需要轉化成從自己的哪個埠出去,很容易想到,應該有個表,就像 MAC 地址表一樣。

    這個表就叫 路由表

    至於這個路由表是怎麽出來的,有很多路由演算法,本文不展開,因為我也不會哈哈~

    不同於 MAC 地址表的是,路由表並不是一對一這種明確關系,我們下面看一個路由表的結構。

    目的地址 子網路遮罩 下一跳
    192.168.0.0 255.255.255.0 0
    192.168.0.254 255.255.255.255 0
    192.168.1.0 255.255.255.0 1
    192.168.1.254 255.255.255.255 1

    我們學習一種新的表示方法,由於子網路遮罩其實就表示前多少位表示子網路的網段,所以如 192.168.0.0(255.255.255.0) 也可以簡寫為 192.168.0.0/24

    目的地址 下一跳
    192.168.0.0/24 0
    192.168.0.254/32 0
    192.168.1.0/24 1
    192.168.1.254/32 1

    這就很好理解了,路由表就表示, 192.168.0.xxx 這個子網路下的,都轉發到 0 號埠,192.168.1.xxx 這個子網路下的,都轉發到 1 號埠 。下一跳列還沒有值,我們先不管

    配合著結構圖來看(這裏把子網路遮罩和預設閘道器都補齊了)圖中 & 筆誤,結果應該是 .0

    剛才說的都是 IP 層,但發送封包的數據鏈路層需要知道 MAC 地址,可是我只知道 IP 地址該怎麽辦呢?

    答案:arp

    假如你(A)此時 不知道 你同伴 B 的 MAC 地址(現實中就是不知道的,剛剛我們只是假設已知),你只知道它的 IP 地址,你該怎麽把封包準確傳給 B 呢?

    答案很簡單,在網路層, 我需要把 IP 地址對應的 MAC 地址找到 ,也就是透過某種方式,找到 192.168.0.2 對應的 MAC 地址 BBBB

    這種方式就是 arp 協定 ,同時電腦 A 和 B 裏面也會有一張 arp 緩存表 ,表中記錄著 IP 與 MAC 地址 對應關系。

    IP 地址 MAC 地址
    192.168.0.2 BBBB

    一開始的時候這個表是 空的 ,電腦 A 為了知道電腦 B(192.168.0.2)的 MAC 地址,將會 廣播 一條 arp 請求,B 收到請求後,帶上自己的 MAC 地址給 A 一個 響應 。此時 A 便更新了自己的 arp 表。

    這樣透過大家不斷廣播 arp 請求,最終所有電腦裏面都將 arp 緩存表更新完整。


    總結一下


    好了,總結一下,到目前為止就幾條規則

    從各個節點的視角來看

    電腦視角

  • 首先我要知道我的 IP 以及對方的 IP

  • 透過子網路遮罩判斷我們是否在同一個子網路

  • 在同一個子網路就透過 arp 獲取對方 mac 地址直接扔出去

  • 不在同一個子網路就透過 arp 獲取預設閘道器的 mac 地址直接扔出去

  • 交換機視角:

  • 我收到的封包必須有目標 MAC 地址

  • 透過 MAC 地址表查對映關系

  • 查到了就按照對映關系從我的指定埠發出去

  • 查不到就所有埠都發出去

  • 路由器視角:

  • 我收到的封包必須有目標 IP 地址

  • 透過路由表查對映關系

  • 查到了就按照對映關系從我的指定埠發出去(不在任何一個子網路範圍,走其路由器的預設閘道器也是查到了)

  • 查不到則返回一個路由不可達的封包

  • 如果你嗅覺足夠敏銳,你應該可以感受到下面這句話

    網路層(IP協定)本身沒有傳輸包的功能,包的實際傳輸是委托給數據鏈路層(乙太網路中的交換機)來實作的。

    涉及到的三張表分別是

  • 交換機中有 MAC 地址 表用於對映 MAC 地址和它的埠

  • 路由器中有 路由表 用於對映 IP 地址(段)和它的埠

  • 電腦和路由器中都有 arp 緩存表 用於緩存 IP 和 MAC 地址的對映關系

  • 這三張表是怎麽來的

  • MAC 地址表是透過乙太網路內各節點之間不斷透過交換機通訊,不斷完善起來的。

  • 路由表是各種路由演算法 + 人工配置逐步完善起來的。

  • arp 緩存表是不斷透過 arp 協定的請求逐步完善起來的。

  • 知道了以上這些,目前網路上兩個節點是如何發送封包的這個過程,就完全可以解釋通了!


    那接下來我們 趁熱打鐵 一下 ,請做好 戰鬥 準備!

    這時路由器 1 連線了路由器 2,所以其路由表有了下一條地址這一個概念,所以它的路由表就變成了這個樣子。如果匹配到了有下一跳地址的一項,則需要再次匹配,找到其埠, 並找到下一跳 IP 的 MAC 地址。

    也就是說找來找去,最終必須能對映到一個埠號,然後從這個埠號把封包發出去。

    目的地址 下一跳
    192.168.0.0/24 0
    192.168.0.254/32 0
    192.168.1.0/24 1
    192.168.1.254/32 1
    192.168.2.0/24 192.168.100.5
    192.168.100.0/24 2
    192.168.100.4/32 2

    這時如果 A 給 F 發送一個封包,能不能通呢?如果通的話整個過程是怎樣的呢?


    思考一分鐘...

    詳細過程動畫描述:

    詳細過程文字描述:

    1. 首先 A (192.168.0.1) 透過子網路遮罩 (255.255.255.0) 計算出自己與 F (192.168.2.2) 並不在同一個子網路內,於是決定發送給預設閘道器 (192.168.0.254)

    2. A 透過 ARP 找到 預設閘道器 192.168.0.254 的 MAC 地址。

    3. A 將源 MAC 地址 (AAAA) 與閘道器 MAC 地址 (ABAB) 封裝在數據鏈路層頭部,又將源 IP 地址 (192.168.0.1) 和目的 IP 地址 (192.168.2.2) (註意這裏千萬不要以為填寫的是預設閘道器的 IP 地址,從始至終這個封包的兩個 IP 地址都是不變的,只有 MAC 地址在不斷變化) 封裝在網路層頭部,然後發包

    4. 交換機 1 收到封包後,發現目標 MAC 地址是 ABAB ,轉發給路由器1

    5. 封包來到了路由器 1,發現其目標 IP 地址是 192.168.2.2 ,檢視其路由表,發現了下一跳的地址是 192.168.100.5

    6. 所以此時路由器 1 需要做兩件事,第一件是再次匹配路由表,發現匹配到了埠為 2,於是將其封裝到數據鏈路層,最後把包從 2 號口 發出去。

    7. 此時路由器 2 收到了封包,看到其目的地址是 192.168.2.2 ,查詢其路由表,匹配到埠號為 1,準備從 1 號口 把封包送出去。

    8. 但此時路由器 2 需要知道 192.168.2.2 的 MAC 地址了,於是檢視其 arp 緩存,找到其 MAC 地址為 FFFF ,將其封裝在數據鏈路層頭部,並從 1 號埠 把包發出去。

    9. 交換機 3 收到了封包,發現目的 MAC 地址為 FFFF ,查詢其 MAC 地址表,發現應該從其 6 號埠 出去,於是從 6 號埠 把封包發出去。

    10. F 最終收到了封包! 並且發現目的 MAC 地址就是自己,於是收下了這個包

    更詳細且精準的過程:

    讀到這相信大家已經很累了,理解上述過程基本上網路層以下的部份主流程就基本疏通了,如果你想要本過程更為專業的過程描述,可以在公眾號 "無聊的閃客" 後台回復 "網路", 獲得我模擬這個過程的 Cisco Packet Tracer 原始檔。

    每一步包的傳輸都會有各層的原始數據,以及專業的過程描述

    同時在此基礎之上你也可以設計自己的網路拓撲結構,進行各種實驗,來加深網路傳輸過程的理解。

    你是不是以為到這裏就結束了?

    不,好戲才剛剛開始!

    請休息一分鐘,我們繼續戰鬥!

    j經過剛剛的一番折騰,只要你知道另一位夥伴 B 的 IP 地址,且你們之間的網路是通的,無論多遠,你都可以將一個封包發送給你的夥伴 B

    這就是實體層、數據鏈路層、網路層這三層所做的事情。

    站在第四層的你,就可以不要臉地利用下三層所做的鋪墊,隨心所欲地發送數據,而不必擔心找不到對方了。

    雖然你此時還什麽都沒幹,但你還是給自己這一層起了個響亮的名字,叫做 傳輸層

    你本以為自己所在的第四層萬事大吉,啥事沒有,但很快問題就接踵而至。



    問題來了



    前三層協定只能把封包從一個主機搬到另外一台主機,但是,到了目的地以後,封包具體交給哪個 程式 (行程)呢?

    所以,你需要把通訊的行程區分開來,於是就給每個行程分配一個數位編號,你給它起了一個響亮的名字: 埠號

    然後你在要發送的封包上,增加了傳輸層的頭部, 源埠號 目標埠號

    OK,這樣你將原本主機到主機的通訊,升級為了 行程和行程之間的通訊

    你沒有意識到,你不知不覺實作了 UDP 協定

    (當然 UDP 協定中不光有源埠和目標埠,還有封包長度和校驗值,我們暫且略過)

    就這樣,你用 UDP 協定無憂無慮地同 B 進行著通訊,一直沒發生什麽問題。

    但很快,你發現事情變得非常復雜......



    丟包問題



    由於網路的不可靠,封包可能在半路遺失,而 A 和 B 卻無法察覺。

    對於丟包問題,只要解決兩個事就好了。

    第一個,A 怎麽知道包丟了?

    答案:讓 B 告訴 A

    第二個,丟了的包怎麽辦?

    答案:重傳

    於是你設計了如下方案,A 每發一個包,都必須收到來自 B 的 確認 (ACK),再發下一個,否則在一定時間內沒有收到確認,就 重傳 這個包。

    你管它叫 停止等待協定 。只要按照這個協定來,雖然 A 無法保證 B 一定能收到包,但 A 能夠確認 B 是否收到了包,收不到就重試,盡最大努力讓這個通訊過程變得可靠,於是你們現在的通訊過程又有了一個新的特征, 可靠交付



    效率問題



    停止等待雖然能解決問題,但是效率太低了,A 原本可以在發完第一個封包之後立刻開始發第二個封包,但由於停止等待協定,A 必須等封包到達了 B ,且 B 的 ACK 包又回到了 A,才可以繼續發第二個封包,這效率慢得可不是一點兩點。

    於是你對這個過程進行了改進,采用 流水線 的方式,不再傻傻地等。



    順序問題



    但是網路是復雜的、不可靠的。

    有的時候 A 發出去的封包,分別走了不同的路由到達 B,可能無法保證和發送封包時一樣的順序。

    在流水線中有多個封包和ACK包在 亂序流動 ,他們之間對應關系就亂掉了。

    難道還回到停止等待協定?A 每收到一個包的確認(ACK)再發下一個包,那就根本不存在順序問題。應該有更好的辦法!

    A 在發送的封包中增加一個 序號 (seq),同時 B 要在 ACK 包上增加一個 確認號 (ack),這樣不但解決了停止等待協定的效率問題,也透過這樣標序號的方式解決了順序問題。

    而 B 這個確認號意味深長:比如 B 發了一個確認號為 ack = 3,它不僅僅表示 A 發送的序號為 2 的包收到了,還表示 2 之前的封包都收到了。這種方式叫 累計確認 累計應答

    註意,實際上 ack 的號是收到的最後一個封包的序號 seq + 1,也就是告訴對方下一個應該發的序號是多少。但圖中為了便於理解,ack 就表示收到的那個序號,不必糾結。



    流量問題



    有的時候,A 發送封包的速度太快,而 B 的接收能力不夠,但 B 卻沒有告知 A 這個情況。

    怎麽解決呢?

    很簡單,B 告訴 A 自己的 接收 能力,A 根據 B 的接收能力,相應控制自己的 發送速率 ,就好了。

    B 怎麽告訴 A 呢?B 跟 A 說"我很強"這三個字麽?那肯定不行,得有一個嚴謹的規範。

    於是 B 決定,每次發送封包給 A 時,順帶傳過來一個值,叫 視窗大小 (win),這個值就表示 B 的 接收能力 。同理,每次 A 給 B 發包時也帶上自己的視窗大小,表示 A 的接收能力。

    B 告訴了 A 自己的視窗大小值,A 怎麽利用它去做 A 這邊發包的流量控制呢?

    很簡單,假如 B 給 A 傳過來的視窗大小 win = 5,那 A 根據這個值,把自己要發送的數據分成這麽幾類。

    圖片過於清晰,就不再文字解釋了。

    當 A 不斷發送封包時, 已發送的最後一個序號 就往右移動,直到碰到了視窗的上邊界,此時 A 就無法繼續發包,達到了流量控制。

    但是當 A 不斷發包的同時,A 也會收到來自 B 的確認包,此時 整個視窗 會往右移動,因此上邊界也往右移動,A 就能發更多的封包了。

    以上都是在視窗大小不變的情況下,而 B 在發給 A 的 ACK 包中,每一個都可以 重新設定 一個新的視窗大小,如果 A 收到了一個新的視窗大小值,A 會隨之調整。

    如果 A 收到了比原視窗值更大的視窗大小,比如 win = 6,則 A 會直接將視窗上邊界向右移動 1 個單位。

    如果 A 收到了比原視窗值小的視窗大小,比如 win = 4,則 A 暫時不會改變視窗大小,更不會將視窗上邊界向左移動,而是等著 ACK 的到來,不斷將左邊界向右移動,直到視窗大小值收縮到新大小為止。

    OK,終於將流量控制問題解決得差不多了,你看著上面一個個小動圖,給這個視窗起了一個更生動的名字, 滑動視窗



    擁塞問題



    但有的時候,不是 B 的接受能力不夠,而是網路不太好,造成了 網路擁塞

    擁塞控制與流量控制有些像,但流量控制是受 B 的接收能力影響,而擁塞控制是受 網路環境 的影響。

    擁塞控制的解決辦法依然是透過設定一定的視窗大小,只不過,流量控制的視窗大小是 B 直接告訴 A 的,而擁塞控制的視窗大小按理說就應該是網路環境主動告訴 A。

    但網路環境怎麽可能主動告訴 A 呢?只能 A 單方面透過 試探 ,不斷感知網路環境的好壞,進而確定自己的擁塞視窗的大小。

    擁塞視窗大小的計算有很多復雜的演算法,就不在本文中展開了,假如 擁塞視窗的大小為 cwnd ,上一部份流量控制的 滑動視窗的大小為 rwnd ,那麽視窗的右邊界受這兩個值共同的影響,需要取它倆的最小值。

    視窗大小 = min(cwnd, rwnd)

    含義很容易理解,當 B 的接受能力比較差時,即使網路非常通暢,A 也需要根據 B 的接收能力限制自己的發送視窗。當網路環境比較差時,即使 B 有很強的接收能力,A 也要根據網路的擁塞情況來限制自己的發送視窗。正所謂受其 短板 的影響嘛~



    連線問題



    有的時候,B 主機的相應行程還沒有準備好或是掛掉了,A 就開始發送封包,導致了浪費。

    這個問題在於,A 在跟 B 通訊之前,沒有事先確認 B 是否已經準備好,就開始發了一連串的資訊。就好比你和另一個人打電話,你還沒有"餵"一下確認對方有沒有在聽,你就巴拉巴拉說了一堆。

    這個問題該怎麽解決呢?

    地球人都知道, 三次握手 嘛!

    A:我準備好了(SYN)

    B:我知道了(ACK),我也準備好了(SYN)

    A:我知道了(ACK)

    A 與 B 各自在記憶體中維護著自己的狀態變量,三次握手之後,雙方的狀態都變成了 連線已建立 (ESTABLISHED)。

    雖然就只是發了三次封包,並且在各自的記憶體中維護了狀態變量,但這麽說總覺得太 low,你看這個過程相當於雙方建立連線的過程,於是你靈機一動,就叫它 面向連線 吧。

    註意:這個連線是虛擬的,是由 A 和 B 這兩個終端共同維護的,在網路中的裝置根本就不知道連線這回事兒!

    但凡事有始就有終,有了建立連線的過程,就要考慮釋放連線的過程,又是地球人都知道, 四次揮手 嘛!

    A:再見,我要關閉了(FIN)

    B:我知道了(ACK)

    給 B 一段時間把自己的事情處理完...

    B:再見,我要關閉了(FIN)

    A:我知道了(ACK)



    總結



    以上講述的,就是 TCP 協定的核心思想,上面過程中需要傳輸的資訊,就體現在 TCP 協定的頭部,這裏放上最常見的 TCP 協定頭解讀的圖。

    不知道你現在再看下面這句話,是否能理解:

    TCP 是

    面向連線的、可靠的、基於字節流的

    傳輸層通訊協定

    面向連線、可靠,這兩個詞透過上面的講述很容易理解,那什麽叫做基於字節流呢?

    很簡單,TCP 在建立連線時,需要告訴對方 MSS(最大報文段大小)。

    也就是說,如果要發送的數據很大,在 TCP 層是需要按照 MSS 來切割成一個個的 TCP 報文段 的。

    切割的時候我才不管你原來的數據表示什麽意思,需要在哪裏斷句啥的,我就把它當成一串毫無意義的字節,在我想要切割的地方哢嚓就來一刀,標上序號,只要接收方再根據這個序號拼成最終想要的完整數據就行了。

    在我 TCP 傳輸這裏,我就把它當做一個個的 字節 ,也就是基於字節流的含義了。

    最後留給大家一個作業,模擬 A 與 B 建立一個 TCP 連線。

    第一題 :A 給 B 發送 "aaa" ,然後 B 給 A 回復一個簡單的字串 "success",並將此過程抓包。

    第二題 :A 給 B 發送 "aaaaaa ... a" 超過最大報文段大小,然後 B 給 A 回復一個簡單的字串 "success",並將此過程抓包

    下面是我抓的包(第二題)

    三次握手階段

    A -> B [SYN] Seq=0 Win=64240 Len=0

    MSS=1460 WS=256

    B - >A [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0

    MSS=1424 WS=512

    A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=0

    數據發送階段

    A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=1424

    A -> B [ACK] Seq=1425 Ack=1 Win=132352 Len=1424

    A -> B [PSH, ACK] Seq=2849 Ack=1 Win=132352 Len=1247

    B -> A [ACK] Seq=1 Ack=1425 Win=32256 Len=0

    B -> A [ACK] Seq=1 Ack=2849 Win=35328 Len=0

    B -> A [ACK] Seq=1 Ack=4096 Win=37888 Len=0

    B -> A [PSH, ACK] Seq=1 Ack=4096 Win=37888 Len=7

    四次揮手階段

    B -> A [FIN, ACK] Seq=8 Ack=4096 Win=37888 Len=0

    A -> B [ACK] Seq=4096 Ack=9 Win=132352 Len=0

    A -> B [FIN, ACK] Seq=4096 Ack=9 Win=132352 Len=0(下面少復制了一行ACK,抱歉)

    詳細的抓包數據與分析整理,就不在文章裏展示了,可以在公眾號 無聊的閃客 後台回復 TCP 獲取。

    後記





    這篇文章是閃客大佬的兩篇經典文章 和 的一個合並和最佳化版本。不少讀者建議合並兩篇文章,效果更炸裂。

    所以時隔近三年重新合並後發出來,希望新老讀者都能喜歡,也順便看看三年後大家是否還喜歡這樣的文章。


    👇🏻 點選下方閱讀原文,獲取魚皮往期編程幹貨

    往期推薦