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中我們就可以直接腦補這種寫法。
用圖來表示,純粹面向物件的時候我們的想法是這樣的:
如果我們把上面的圖倒過來,就變成了面向介面:
在使用面向介面之後,我們就是做了這樣一種假設:
def push(pusher, id): pusher.push_by_id(id)
即,傳給push函式的pusher例項一定存在push_by_id
方法。正是基於這樣一種假設,
我們得以把具體業務程式碼和具體的推送商劃分開來,這就是所謂的抽象,也就是一種分層。
要分層的原因也就顯現出來了,為了把不同的東西錯綜複雜的關係劃分開來,也就是古話 說的"快刀斬亂麻"的這種感覺。
兩種網路模型
日常程式設計裡我們用的最多的就是TCP了,UDP也是有的,但是很少,舉一些常見的例子:
當然了,這只是常見實現方式如此,其實用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個bit來表示,也就是說這中間不能傳送多於 2 ** 16 個bit。
所以協議變成了:
- 最開始我們傳送111表示資訊開始
- 隨後我們用16個bit表示包的長度
- 然後,我們先有48個bit表示傳送者的MAC地址,再有48個bit表示接受者的MAC地址
- 之後,就是我們要傳送的資訊
- 最後我們傳送000表示結束,如果開頭和結尾不是這樣的,那麼說明這是假的資訊。
傳送者地址+接收者地址+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來表示,因為ip裡面已經待了ip地址,我這裡就不重複帶了
- 然後是目標地址的埠號,8個bit來表示
- 然後是這個包的序號,8個bit來表示
但是我們說好了要把這個協議打造成一個可靠的協議,可不能食言。我想想,怎麼讓他 可靠呢,無非就是我發一個資訊,你告訴我你收到了,要是你不告訴我,我就發到你告訴我 為止。差不多就是這麼個意思。但是呢,又不想構造多個不同的協議,你知道,程式設計的時候 要是寫一堆的if-else樹那可就很蛋疼了。再改改協議:
- 首先是來源地址的埠號,8個bit來表示,因為ip裡面已經待了ip地址,我這裡就不重複帶了
- 然後是目標地址的埠號,8個bit來表示
- 然後是這個包的序號,8個bit來表示
- 然後是想確認的包的序號,8個bit來表示
咦,點睛之筆耶,這個確認的包的序號,因為我們是雙向通訊,我發他資訊的時候還可以順便 確認我收到了他的包啊,真是一箭雙鵰。
TCP是一個面向流的協議,什麼叫流?車流,水流,車流比較形象。車和車之間是分開的, 但是速度一快起來,就可以把它們看成連起來的。TCP也是這樣,單個包之間是分開的, 但是卻可以看作是連起來,為什麼呢?因為每個包裡都帶了ip地址和埠號,ip地址和埠 號一樣的,就可以看作是連起來的 :)
所以我們可以想象一下,我們的ip地址是192.168.1.1
, 埠號是 1, 目標的ip地址是192.168.1.2
, 埠號是 2。那我們傳送這樣的包:
111(開始) 00000000 11101000(長度) 01110010 01101111 01110101 01110100 01100101 01110010(來源MAC地址) 01110000 01101000 01101111 01101110 01100101 01110010(目標MAC地址) 11000000 10101000 00000001 00000001(來源ip地址) 11000000 10101000 00000001 00000010(目標ip地址) 00000001(來源的埠號) 00000010(目標的埠號) 00000001(傳送的包的序號是1) 00000000(已經確認的包的序號是0,表示啥都沒有嘛) 01101000 01100101 01101100 01101100 01101111(字串"hello") 000(結束)
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 Aug8 20:41:04 CST 2017</center> </body> </html> ^] telnet> Connection closed.
輸入GET / HTTP/1.1
之後回車,百度就給我返回了下面的一長串,然後瀏覽器再根據
返回的內容進行渲染,這又是一個大話題了,不講了不講了,收工 :)