天天看點

從零建構TCP/IP協定

從零建構tcp/ip協定(這次叫pct協定)

這篇部落格是讀完《圖解tcp/ip協定》和《tcp/ip協定詳解卷一:協定》之後的總結

我從0建構了一個可靠的雙工的有序的基于流的協定,叫做pct協定 :)

osi七層模型和tcp/ip四層模型

談到計算機網絡,就一定會說起osi七層模型和tcp/ip四層模型,不過我們先從為何分層 說起。

為什麼要分層

軟體開發的過程中,我們經常聽到的詞語是"解耦","高内聚,低耦合"等等諸如此類的 詞語,又常聽見寫java的同學念叨着"橋接模式","面向接口"等詞語,那麼他們說的這些 詞語的核心問題是什麼呢?我們先從一個簡單的問題看起:

現在我們需要做一個推送系統,要對接android和ios兩個系統,大家都知道,apple有統一

的推送管道,apns,是以我們隻要接入這個就好,但是android的推送在國内是百家争鳴,

就拿之前我為公司接入推送通知來舉例,要接入極光,小米,可能要接入華為推送。

那我要怎麼從具體的推送裡抽象出來呢?運用面向對象的想法,我們很容易就能想到, 我們有一個父類,叫 basepush ,他的子類就是具體的

mipush , jpush , hmspush 。 父類中有 push_by_id 和 push_by_tag

等方法,子類重寫。這樣我們在具體實作的時候 執行個體化子類,并且調用對應的方法就好。這種思想其實就是面向接口程式設計,在java中我們

可以轉變一下程式設計的寫法,把繼承變成接口。在python中我們就可以直接腦補這種寫法。 用圖來表示,純粹面向對象的時候我們的想法是這樣的:

從零建構TCP/IP協定

如果我們把上面的圖倒過來,就變成了面向接口:

從零建構TCP/IP協定

在使用面向接口之後,我們就是做了這樣一種假設:

def push(pusher, id): 

    pusher.push_by_id(id) 

即,傳給push函數的pusher執行個體一定存在 push_by_id 方法。正是基于這樣一種假設, 我們得以把具體業務代碼和具體的推送商劃分開來,這就是所謂的抽象,也就是一種分層。

要分層的原因也就顯現出來了,為了把不同的東西錯綜複雜的關系劃分開來,也就是古話 說的"快刀斬亂麻"的這種感覺。

兩種網絡模型

日常程式設計裡我們用的最多的就是tcp了,udp也是有的,但是很少,舉一些常見的例子:

dns -> udp

連接配接mysql -> tcp

連接配接redis -> tcp

rpc -> tcp

通路網站 -> tcp

當然了,這隻是常見實作方式如此,其實用udp也是可以實作的。這篇部落格裡我們暫時不讨論 udp。我們先來看tcp/ip四層是怎麼分層的:

ascii 表格其實挺好看的,最後渲染的時候因為寬字元的原因格式有點亂掉了,下同

+------------+-----------------------+ 

| 層         | 例如                  | 

| 應用層     | http協定              | 

| 傳輸層     | tcp                   | 

| 網絡互連層 | ip                    | 

| 網絡接口層 | 如網線,雙絞線,wi-fi | 

我們直接把 tcp/ip 四層協定 映射到 osi七層協定 上看:

+--------------+---------------+----------------+ 

| osi 七層協定 | 例如          | 對應tcp/ip四層 | 

| 應用層       | http協定      |                | 

+--------------+---------------+                | 

| 表示層       |               | 應用層         | 

| 會話層       |               |                | 

| 傳輸層       | tcp           | 傳輸層         | 

| 網絡層       | ip            | 網際層         | 

| 資料鍊路層   | 網際網路,wi-fi |                | 

+--------------+---------------+ 網絡接口層     | 

| 實體層       | 雙絞線,光纜  |                | 

接下來我們将從底層逐層向上來解析網絡,最後我們将簡略的介紹tcp(tcp的知識足夠 寫好幾本書,一篇部落格裡遠遠介紹不完。不信可以看看tcp/ip協定詳解那三卷書加起來 有多厚)。

實體層

實體層,顧名思義,就是實體的,可見的東西。也就是平時我們所說的光纖,wi-fi(無線電波)

等,我們知道計算機是用0和1來表示的,對應到不同的媒體裡是不同的表現形式,

是以為了把實體層的實作屏蔽掉,我們把這些都分到一層裡,例如wi-fi通過波的

波峰與波谷可以表示出0和1的狀态(我們平時會說成1和-1,對應計算機裡其實就是1和0)。

對應到電裡,我們可以用高電壓和低電壓來表示出1和0。如同最開始講的例子一樣,

我們不管具體的媒體是什麼,隻知道,我們用的這個媒體有辦法表示1和0。

資料鍊路層

如果我們去郵局寫一封信,填完收件人之後,郵局派發的順序可能是,先投遞到指定的 國家,然後投遞到具體的省,然後市。。。逐次投遞下去。那麼我們玩電腦的時候,計算機 要怎麼把a發給b的資訊準确送達呢?

肯定大家都要有一個位址,上一節我們知道了,不同的媒體都有他的方式表示1和0,那麼

我們給媒體的兩端加上位址,我們叫做mac位址,如何?就拿路由器來說吧,路由器的 mac位址叫做 router ,手機的mac位址叫做

phoner ,為了表示成0和1,我們分别取 字元串的ascii的二進制來表示,路由器叫做 1110010 1101111 1110101

1110100 1100101 1110010 , 而手機則叫做: 1110000 1101000 1101111 1101110

1100101 1110010 ,現在我們終于可以發資訊 了,最少是相鄰的兩個東西可以透過某種媒體來發資訊,是以我們定下這樣的協定:

協定,其實就是一種約定 :)

最開始我們發送111表示資訊開始

然後,我們先有48個bit表示發送者的mac位址,再有48個bit表示接受者的mac位址

之後,就是我們要發送的資訊

最後我們發送000表示結束,如果開頭和結尾不是這樣的,那麼說明這是假的資訊。

知道上面為啥手機叫 phoner 而不叫 phone 了嘛 :) 就是為了保證地指名長度一樣

"hello" 的二進制表示是 "1101000 1100101 1101100 1101100 1101111",如果路由器要向 手機發送 "hello"的話,那麼就發送這樣一串二進制(用換行分割,這樣更容易看清楚):

這樣表示看起來可行,不過遇到一個問題,就是如果這一串二進制中間就出現了000怎麼辦? 因為計算機讀取的時候是從頭開始讀的,這樣子計算機就會亂掉。

為了解決這個問題,我們修改一下協定,在111之後加上發送者位址+接受者位址+所要發送的 資訊的長度。我們用 16個位元組來表示,也就是說這中間不能發送多于 2 ** 16 個bit。

是以協定變成了:

随後我們用16個bit表示包的長度

發送者位址+接收者位址+hello的bit長度是 6 * 8 + 6 * 8 + 5 * 8 = 136,二進制表示 為: 00000000 10001000

是以發送的整個資訊變成了:

網絡層

現在我們終于可以發送資訊了。不過有個缺點,我們隻能在相鄰的時候才可以發送資訊, 那有沒有辦法可以借助兩兩傳遞,在不同的地方也發送資訊呢?有,那就是我們的網絡層 也就是ip(我們能遇到的最通俗易懂的一個名詞了,暫時把它當作網絡層的代名詞也不為過)。

剛剛我們已經學會了一種技術,就是配置設定一個位址,剛剛的叫做mac位址,我們用來做 相鄰兩個節點的定位。其實這個位址也可以用來在多個節點之間找人,基于這樣一種 技術:每個節點都知道和自己相鄰的節點的mac位址,那麼,比如這樣一種連接配接方式:

a - b - c - e 

 \     / 

  - d - 

a向e發送消息,就可以這樣:

a向b和d發消息:給我發到e去

b和d接到之後發現來源是a,是以就隻給c發消息:給我發到e去

c接到消息之後發現來源是b和d,是以就給e發消息:給我發到e去

e接到消息之後發現接收方是自己,是以就把消息吞了

你别說,這種方式好像真的行得通呢,除了有一個顯著的問題,a向e發送一份消息, 最後e收到了兩份,這個我們需要到後面進行去重。我們先打上一個todo的标簽吧。

還有一個細節問題,不知道大家發現了麼,剛才我們說過,mac位址是相鄰兩個節點

通信用的,裡面有來源位址和目标位址,如果我們向上面這樣傳輸的話,每個節點都

隻是把裡面的資訊傳過去,但是來源位址卻改要改寫成自己的mac位址,要不然的話,

b就不知道資訊是a發來的還是c發來的呀,對不對?那問題就來了,e要怎麼知道資訊 其實是從a發過來的呢?

沒辦法了,我們隻好在傳輸的資訊裡把真正的來源位址寫進去,是以我們又定了一個 協定,我們管它叫做ip:

mac攜帶的資訊的開始,是來源的ip位址,32個bit表示

然後是目标的ip位址,32個bit表示

然後是我要帶的資訊

那和上面的資料鍊路層的協定合一下起來,假設來源位址是 192.168.1.1 ,目标位址是 192.168.1.2 ,發送的資訊還是 "hello",整個包就像這樣:

111(開始) 

00000000 11001000(長度) 

01110010 01101111 01110101 01110100 01100101 01110010(來源mac位址) 

01110000 01101000 01101111 01101110 01100101 01110010(目标mac位址) 

11000000 10101000 00000001 00000001(來源ip位址) 

11000000 10101000 00000001 00000010(目标ip位址) 

01101000 01100101 01101100 01101100 01101111(字元串"hello") 

000(結束) 

這樣是不是就很科學?那必須的。哎呀,終于可以跨節點發送消息了,小開心~

可是還是有問題,如果我想确定a發的資訊一定送達了e怎麼辦?怎麼提供可靠性?ip這一層 并不提供可靠性,隻是說盡量送達。看來有必要再來一層!

傳輸層

我們知道,一台計算機上可能有很多個程式在運作,那怎麼區分不同的程式呢?是以我們 給程式加上了id,叫做pid。那計算機網絡通信的時候怎麼區分呢?又假設n個程序想和另外 一台機器上的某一個程序通信呢?怎麼辦?

不如我們再配置設定一個id吧,他們共同持有這個id就好了。我們把這個id叫做端口(port)。 這樣子的話,通過ip位址我們可以确定計算機,通過端口我們可以确定一個或多個程序。

我們繼續造協定,不過這一次我們想要這個協定賊可靠,是以要多做一些工作。其實要是 按照七層協定來實作的話,完全不必在這一層幹這麼多事情,不同的層幹不同的事情嘛, 對不對。不過為了了解tcp協定,我們呀,也跟着來自己捏造一個協定,不如叫pct好了。

繼續,我們要在ip帶的資訊裡規定好我們這樣發:

首先是來源位址的端口号,8個bit來表示,因為ip裡面已經待了ip位址,我這裡就不重複帶了

然後是目标位址的端口号,8個bit來表示

這樣,簡單的pct協定就做好了。

還有一個問題,就是我們要保證發出去的資訊是有序的,因為可能有的資訊走光纖, 有的資訊走wi-fi,他們傳輸速率不一樣嘛。

是以我們在協定裡這樣寫:

然後是這個包的序号,8個bit來表示

但是我們說好了要把這個協定打造成一個可靠的協定,可不能食言。我想想,怎麼讓他

可靠呢,無非就是我發一個資訊,你告訴我你收到了,要是你不告訴我,我就發到你告訴我

為止。差不多就是這麼個意思。但是呢,又不想構造多個不同的協定,你知道,程式設計的時候 要是寫一堆的if-else樹那可就很蛋疼了。再改改協定:

然後是想确認的包的序号,8個bit來表示

咦,點睛之筆耶,這個确認的包的序号,因為我們是雙向通信,我發他資訊的時候還可以順便 确認我收到了他的包啊,真是一箭雙雕。

tcp是一個面向流的協定,什麼叫流?車流,水流,車流比較形象。車和車之間是分開的,

但是速度一快起來,就可以把它們看成連起來的。tcp也是這樣,單個包之間是分開的,

但是卻可以看作是連起來,為什麼呢?因為每個包裡都帶了ip位址和端口号,ip位址和端口 号一樣的,就可以看作是連起來的 :)

是以我們可以想象一下,我們的ip位址是 192.168.1.1 , 端口号是 1, 目标的ip位址是 192.168.1.2 , 端口号是 2。那我們發送這樣的包:

00000000 11101000(長度) 

00000001(來源的端口号) 

00000010(目标的端口号) 

00000001(發送的包的序号是1) 

00000000(已經确認的包的序号是0,表示啥都沒有嘛) 

duang,就這樣,我們建構起了屬于自己的可靠的基于流的雙工的協定 :)

順便我們還完成了上面的todo,通過序号我們就可以判斷這個包是不是重複了,哈哈哈, 一箭n雕~

tcp三次握手四次揮手滑動視窗擁塞控制等就不講了,還是去看《tcp/ip協定詳解卷一》吧 :)

應用層

這下我們終于可以放心大膽的發送消息了,pct協定是個負責任的協定,如果能送到,他就一定 會送到,并且是有序的,要是網絡壞掉了,實在連不上,他就會告訴我網絡連不上。

這樣子來程式設計友善多了呀。

現在我想知道浏覽器和伺服器是怎麼通信的。我們來看看百度。

$ telnet www.baidu.com 80 

trying 183.232.231.173... 

connected to www.baidu.com. 

escape character is '^]'. 

get / http/1.1 

http/1.1 302 moved temporarily 

date: sat, 12 aug 2017 10:45:14 gmt 

content-type: text/html 

content-length: 215 

connection: keep-alive 

location: http://www.baidu.com/search/error.html 

server: bws/1.1 

x-ua-compatible: ie=edge,chrome=1 

bdpagetype: 3 

set-cookie: bdsvrtm=0; path=/ 

<html> 

<head><title>302 found</title></head> 

<body bgcolor="white"> 

<center><h1>302 found</h1></center> 

<hr><center>pr-nginx_1-0-350_branch branch 

time : tue aug  8 20:41:04 cst 2017</center> 

</body> 

</html> 

^] 

telnet>  

connection closed. 

輸入 get / http/1.1 之後回車,百度就給我傳回了下面的一長串,然後浏覽器再根據 傳回的内容進行渲染,這又是一個大話題了,不講了不講了,收工 :)

作者:佚名

來源:51cto

繼續閱讀