從零構(gòu)建TCP/IP協(xié)議
從零構(gòu)建TCP/IP協(xié)議(這次叫PCT協(xié)議)
這篇博客是讀完《圖解TCP/IP協(xié)議》和《TCP/IP協(xié)議詳解卷一:協(xié)議》之后的總結(jié)
我從0構(gòu)建了一個(gè)可靠的雙工的有序的基于流的協(xié)議,叫做PCT協(xié)議 :)
OSI七層模型和TCP/IP四層模型
談到計(jì)算機(jī)網(wǎng)絡(luò),就一定會(huì)說起OSI七層模型和TCP/IP四層模型,不過我們先從為何分層 說起。
為什么要分層
軟件開發(fā)的過程中,我們經(jīng)常聽到的詞語是"解耦","高內(nèi)聚,低耦合"等等諸如此類的 詞語,又常聽見寫Java的同學(xué)念叨著"橋接模式","面向接口"等詞語,那么他們說的這些 詞語的核心問題是什么呢?我們先從一個(gè)簡單的問題看起:
現(xiàn)在我們需要做一個(gè)推送系統(tǒng),要對接Android和iOS兩個(gè)系統(tǒng),大家都知道,Apple有統(tǒng)一 的推送渠道,APNs,所以我們只要接入這個(gè)就好,但是Android的推送在國內(nèi)是百家爭鳴, 就拿之前我為公司接入推送通知來舉例,要接入極光,小米,可能要接入華為推送。
那我要怎么從具體的推送里抽象出來呢?運(yùn)用面向?qū)ο蟮南敕?,我們很容易就能想到?我們有一個(gè)父類,叫 BasePush ,他的子類就是具體的 MiPush , JPush , HMSPush 。 父類中有 push_by_id 和 push_by_tag 等方法,子類重寫。這樣我們在具體實(shí)現(xiàn)的時(shí)候 實(shí)例化子類,并且調(diào)用對應(yīng)的方法就好。這種思想其實(shí)就是面向接口編程,在Java中我們 可以轉(zhuǎn)變一下編程的寫法,把繼承變成接口。在Python中我們就可以直接腦補(bǔ)這種寫法。 用圖來表示,純粹面向?qū)ο蟮臅r(shí)候我們的想法是這樣的:
如果我們把上面的圖倒過來,就變成了面向接口:
在使用面向接口之后,我們就是做了這樣一種假設(shè):
- def push(pusher, id):
- pusher.push_by_id(id)
即,傳給push函數(shù)的pusher實(shí)例一定存在 push_by_id 方法。正是基于這樣一種假設(shè), 我們得以把具體業(yè)務(wù)代碼和具體的推送商劃分開來,這就是所謂的抽象,也就是一種分層。
要分層的原因也就顯現(xiàn)出來了,為了把不同的東西錯(cuò)綜復(fù)雜的關(guān)系劃分開來,也就是古話 說的"快刀斬亂麻"的這種感覺。
兩種網(wǎng)絡(luò)模型
日常編程里我們用的最多的就是TCP了,UDP也是有的,但是很少,舉一些常見的例子:
- DNS -> UDP
- 連接MySQL -> TCP
- 連接Redis -> TCP
- RPC -> TCP
- 訪問網(wǎng)站 -> TCP
當(dāng)然了,這只是常見實(shí)現(xiàn)方式如此,其實(shí)用UDP也是可以實(shí)現(xiàn)的。這篇博客里我們暫時(shí)不討論 UDP。我們先來看TCP/IP四層是怎么分層的:
ascii 表格其實(shí)挺好看的,最后渲染的時(shí)候因?yàn)閷捵址脑蚋袷接悬c(diǎn)亂掉了,下同
- +------------+-----------------------+
- | 層 | 例如 |
- +------------+-----------------------+
- | 應(yīng)用層 | HTTP協(xié)議 |
- +------------+-----------------------+
- | 傳輸層 | TCP |
- +------------+-----------------------+
- | 網(wǎng)絡(luò)互連層 | IP |
- +------------+-----------------------+
- | 網(wǎng)絡(luò)接口層 | 如網(wǎng)線,雙絞線,Wi-Fi |
- +------------+-----------------------+
我們直接把 TCP/IP 四層協(xié)議 映射到 OSI七層協(xié)議 上看:
- +--------------+---------------+----------------+
- | OSI 七層協(xié)議 | 例如 | 對應(yīng)TCP/IP四層 |
- +--------------+---------------+----------------+
- | 應(yīng)用層 | HTTP協(xié)議 | |
- +--------------+---------------+ |
- | 表示層 | | 應(yīng)用層 |
- +--------------+---------------+ |
- | 會(huì)話層 | | |
- +--------------+---------------+----------------+
- | 傳輸層 | TCP | 傳輸層 |
- +--------------+---------------+----------------+
- | 網(wǎng)絡(luò)層 | IP | 網(wǎng)際層 |
- +--------------+---------------+----------------+
- | 數(shù)據(jù)鏈路層 | 因特網(wǎng),Wi-Fi | |
- +--------------+---------------+ 網(wǎng)絡(luò)接口層 |
- | 物理層 | 雙絞線,光纜 | |
- +--------------+---------------+----------------+
接下來我們將從底層逐層向上來解析網(wǎng)絡(luò),最后我們將簡略的介紹TCP(TCP的知識(shí)足夠 寫好幾本書,一篇博客里遠(yuǎn)遠(yuǎn)介紹不完。不信可以看看TCP/IP協(xié)議詳解那三卷書加起來 有多厚)。
物理層
物理層,顧名思義,就是物理的,可見的東西。也就是平時(shí)我們所說的光纖,Wi-Fi(無線電波) 等,我們知道計(jì)算機(jī)是用0和1來表示的,對應(yīng)到不同的介質(zhì)里是不同的表現(xiàn)形式, 因此為了把物理層的實(shí)現(xiàn)屏蔽掉,我們把這些都分到一層里,例如Wi-Fi通過波的 波峰與波谷可以表示出0和1的狀態(tài)(我們平時(shí)會(huì)說成1和-1,對應(yīng)計(jì)算機(jī)里其實(shí)就是1和0)。 對應(yīng)到電里,我們可以用高電壓和低電壓來表示出1和0。如同最開始講的例子一樣, 我們不管具體的介質(zhì)是什么,只知道,我們用的這個(gè)介質(zhì)有辦法表示1和0。
數(shù)據(jù)鏈路層
如果我們?nèi)ム]局寫一封信,填完收件人之后,郵局派發(fā)的順序可能是,先投遞到指定的 國家,然后投遞到具體的省,然后市。。。逐次投遞下去。那么我們玩電腦的時(shí)候,計(jì)算機(jī) 要怎么把A發(fā)給B的信息準(zhǔn)確送達(dá)呢?
肯定大家都要有一個(gè)地址,上一節(jié)我們知道了,不同的介質(zhì)都有他的方式表示1和0,那么 我們給介質(zhì)的兩端加上地址,我們叫做MAC地址,如何?就拿路由器來說吧,路由器的 MAC地址叫做 router ,手機(jī)的MAC地址叫做 phoner ,為了表示成0和1,我們分別取 字符串的ASCII的二進(jìn)制來表示,路由器叫做 1110010 1101111 1110101 1110100 1100101 1110010 , 而手機(jī)則叫做: 1110000 1101000 1101111 1101110 1100101 1110010 ,現(xiàn)在我們終于可以發(fā)信息 了,最少是相鄰的兩個(gè)東西可以透過某種介質(zhì)來發(fā)信息,所以我們定下這樣的協(xié)議:
協(xié)議,其實(shí)就是一種約定 :)
- 最開始我們發(fā)送111表示信息開始
- 然后,我們先有48個(gè)bit表示發(fā)送者的MAC地址,再有48個(gè)bit表示接受者的MAC地址
- 之后,就是我們要發(fā)送的信息
- 最后我們發(fā)送000表示結(jié)束,如果開頭和結(jié)尾不是這樣的,那么說明這是假的信息。
知道上面為啥手機(jī)叫 phoner 而不叫 phone 了嘛 :) 就是為了保證地指名長度一樣
"hello" 的二進(jìn)制表示是 "1101000 1100101 1101100 1101100 1101111",如果路由器要向 手機(jī)發(fā)送 "hello"的話,那么就發(fā)送這樣一串二進(jìn)制(用換行分割,這樣更容易看清楚):
這樣表示看起來可行,不過遇到一個(gè)問題,就是如果這一串二進(jìn)制中間就出現(xiàn)了000怎么辦? 因?yàn)橛?jì)算機(jī)讀取的時(shí)候是從頭開始讀的,這樣子計(jì)算機(jī)就會(huì)亂掉。
為了解決這個(gè)問題,我們修改一下協(xié)議,在111之后加上發(fā)送者地址+接受者地址+所要發(fā)送的 信息的長度。我們用 16個(gè)字節(jié)來表示,也就是說這中間不能發(fā)送多于 2 ** 16 個(gè)bit。
所以協(xié)議變成了:
- 最開始我們發(fā)送111表示信息開始
- 隨后我們用16個(gè)bit表示包的長度
- 然后,我們先有48個(gè)bit表示發(fā)送者的MAC地址,再有48個(gè)bit表示接受者的MAC地址
- 之后,就是我們要發(fā)送的信息
- 最后我們發(fā)送000表示結(jié)束,如果開頭和結(jié)尾不是這樣的,那么說明這是假的信息。
發(fā)送者地址+接收者地址+hello的bit長度是 6 * 8 + 6 * 8 + 5 * 8 = 136,二進(jìn)制表示 為: 00000000 10001000
所以發(fā)送的整個(gè)信息變成了:
網(wǎng)絡(luò)層
現(xiàn)在我們終于可以發(fā)送信息了。不過有個(gè)缺點(diǎn),我們只能在相鄰的時(shí)候才可以發(fā)送信息, 那有沒有辦法可以借助兩兩傳遞,在不同的地方也發(fā)送信息呢?有,那就是我們的網(wǎng)絡(luò)層 也就是ip(我們能遇到的最通俗易懂的一個(gè)名詞了,暫時(shí)把它當(dāng)作網(wǎng)絡(luò)層的代名詞也不為過)。
剛剛我們已經(jīng)學(xué)會(huì)了一種技術(shù),就是分配一個(gè)地址,剛剛的叫做MAC地址,我們用來做 相鄰兩個(gè)節(jié)點(diǎn)的定位。其實(shí)這個(gè)地址也可以用來在多個(gè)節(jié)點(diǎn)之間找人,基于這樣一種 技術(shù):每個(gè)節(jié)點(diǎn)都知道和自己相鄰的節(jié)點(diǎn)的MAC地址,那么,比如這樣一種連接方式:
- A - B - C - E
- \ /
- - D -
- A向E發(fā)送消息,就可以這樣:
- A向B和D發(fā)消息:給我發(fā)到E去
- B和D接到之后發(fā)現(xiàn)來源是A,所以就只給C發(fā)消息:給我發(fā)到E去
- C接到消息之后發(fā)現(xiàn)來源是B和D,所以就給E發(fā)消息:給我發(fā)到E去
- E接到消息之后發(fā)現(xiàn)接收方是自己,所以就把消息吞了
你別說,這種方式好像真的行得通呢,除了有一個(gè)顯著的問題,A向E發(fā)送一份消息, 最后E收到了兩份,這個(gè)我們需要到后面進(jìn)行去重。我們先打上一個(gè)TODO的標(biāo)簽吧。
還有一個(gè)細(xì)節(jié)問題,不知道大家發(fā)現(xiàn)了么,剛才我們說過,MAC地址是相鄰兩個(gè)節(jié)點(diǎn) 通信用的,里面有來源地址和目標(biāo)地址,如果我們向上面這樣傳輸?shù)脑?,每個(gè)節(jié)點(diǎn)都 只是把里面的信息傳過去,但是來源地址卻改要改寫成自己的MAC地址,要不然的話, B就不知道信息是A發(fā)來的還是C發(fā)來的呀,對不對?那問題就來了,E要怎么知道信息 其實(shí)是從A發(fā)過來的呢?
沒辦法了,我們只好在傳輸?shù)男畔⒗锇颜嬲膩碓吹刂穼戇M(jìn)去,所以我們又定了一個(gè) 協(xié)議,我們管它叫做ip:
- MAC攜帶的信息的開始,是來源的ip地址,32個(gè)bit表示
- 然后是目標(biāo)的ip地址,32個(gè)bit表示
- 然后是我要帶的信息
那和上面的數(shù)據(jù)鏈路層的協(xié)議合一下起來,假設(shè)來源地址是 192.168.1.1 ,目標(biāo)地址是 192.168.1.2 ,發(fā)送的信息還是 "hello",整個(gè)包就像這樣:
- 111(開始)
- 00000000 11001000(長度)
- 01110010 01101111 01110101 01110100 01100101 01110010(來源MAC地址)
- 01110000 01101000 01101111 01101110 01100101 01110010(目標(biāo)MAC地址)
- 11000000 10101000 00000001 00000001(來源ip地址)
- 11000000 10101000 00000001 00000010(目標(biāo)ip地址)
- 01101000 01100101 01101100 01101100 01101111(字符串"hello")
- 000(結(jié)束)
這樣是不是就很科學(xué)?那必須的。哎呀,終于可以跨節(jié)點(diǎn)發(fā)送消息了,小開心~
可是還是有問題,如果我想確定A發(fā)的信息一定送達(dá)了E怎么辦?怎么提供可靠性?IP這一層 并不提供可靠性,只是說盡量送達(dá)。看來有必要再來一層!
傳輸層
我們知道,一臺(tái)計(jì)算機(jī)上可能有很多個(gè)程序在運(yùn)行,那怎么區(qū)分不同的程序呢?所以我們 給程序加上了id,叫做pid。那計(jì)算機(jī)網(wǎng)絡(luò)通信的時(shí)候怎么區(qū)分呢?又假設(shè)n個(gè)進(jìn)程想和另外 一臺(tái)機(jī)器上的某一個(gè)進(jìn)程通信呢?怎么辦?
不如我們再分配一個(gè)id吧,他們共同持有這個(gè)id就好了。我們把這個(gè)id叫做端口(port)。 這樣子的話,通過ip地址我們可以確定計(jì)算機(jī),通過端口我們可以確定一個(gè)或多個(gè)進(jìn)程。
我們繼續(xù)造協(xié)議,不過這一次我們想要這個(gè)協(xié)議賊可靠,所以要多做一些工作。其實(shí)要是 按照七層協(xié)議來實(shí)現(xiàn)的話,完全不必在這一層干這么多事情,不同的層干不同的事情嘛, 對不對。不過為了理解TCP協(xié)議,我們呀,也跟著來自己捏造一個(gè)協(xié)議,不如叫PCT好了。
繼續(xù),我們要在ip帶的信息里規(guī)定好我們這樣發(fā):
- 首先是來源地址的端口號(hào),8個(gè)bit來表示,因?yàn)閕p里面已經(jīng)待了ip地址,我這里就不重復(fù)帶了
- 然后是目標(biāo)地址的端口號(hào),8個(gè)bit來表示
這樣,簡單的PCT協(xié)議就做好了。
還有一個(gè)問題,就是我們要保證發(fā)出去的信息是有序的,因?yàn)榭赡苡械男畔⒆吖饫w, 有的信息走Wi-Fi,他們傳輸速率不一樣嘛。
所以我們在協(xié)議里這樣寫:
- 首先是來源地址的端口號(hào),8個(gè)bit來表示,因?yàn)閕p里面已經(jīng)待了ip地址,我這里就不重復(fù)帶了
- 然后是目標(biāo)地址的端口號(hào),8個(gè)bit來表示
- 然后是這個(gè)包的序號(hào),8個(gè)bit來表示
但是我們說好了要把這個(gè)協(xié)議打造成一個(gè)可靠的協(xié)議,可不能食言。我想想,怎么讓他 可靠呢,無非就是我發(fā)一個(gè)信息,你告訴我你收到了,要是你不告訴我,我就發(fā)到你告訴我 為止。差不多就是這么個(gè)意思。但是呢,又不想構(gòu)造多個(gè)不同的協(xié)議,你知道,編程的時(shí)候 要是寫一堆的if-else樹那可就很蛋疼了。再改改協(xié)議:
- 首先是來源地址的端口號(hào),8個(gè)bit來表示,因?yàn)閕p里面已經(jīng)待了ip地址,我這里就不重復(fù)帶了
- 然后是目標(biāo)地址的端口號(hào),8個(gè)bit來表示
- 然后是這個(gè)包的序號(hào),8個(gè)bit來表示
- 然后是想確認(rèn)的包的序號(hào),8個(gè)bit來表示
咦,點(diǎn)睛之筆耶,這個(gè)確認(rèn)的包的序號(hào),因?yàn)槲覀兪请p向通信,我發(fā)他信息的時(shí)候還可以順便 確認(rèn)我收到了他的包啊,真是一箭雙雕。
TCP是一個(gè)面向流的協(xié)議,什么叫流?車流,水流,車流比較形象。車和車之間是分開的, 但是速度一快起來,就可以把它們看成連起來的。TCP也是這樣,單個(gè)包之間是分開的, 但是卻可以看作是連起來,為什么呢?因?yàn)槊總€(gè)包里都帶了ip地址和端口號(hào),ip地址和端口 號(hào)一樣的,就可以看作是連起來的 :)
所以我們可以想象一下,我們的ip地址是 192.168.1.1 , 端口號(hào)是 1, 目標(biāo)的ip地址是 192.168.1.2 , 端口號(hào)是 2。那我們發(fā)送這樣的包:
- 111(開始)
- 00000000 11101000(長度)
- 01110010 01101111 01110101 01110100 01100101 01110010(來源MAC地址)
- 01110000 01101000 01101111 01101110 01100101 01110010(目標(biāo)MAC地址)
- 11000000 10101000 00000001 00000001(來源ip地址)
- 11000000 10101000 00000001 00000010(目標(biāo)ip地址)
- 00000001(來源的端口號(hào))
- 00000010(目標(biāo)的端口號(hào))
- 00000001(發(fā)送的包的序號(hào)是1)
- 00000000(已經(jīng)確認(rèn)的包的序號(hào)是0,表示啥都沒有嘛)
- 01101000 01100101 01101100 01101100 01101111(字符串"hello")
- 000(結(jié)束)
duang,就這樣,我們構(gòu)建起了屬于自己的可靠的基于流的雙工的協(xié)議 :)
順便我們還完成了上面的TODO,通過序號(hào)我們就可以判斷這個(gè)包是不是重復(fù)了,哈哈哈, 一箭n雕~
TCP三次握手四次揮手滑動(dòng)窗口擁塞控制等就不講了,還是去看《TCP/IP協(xié)議詳解卷一》吧 :)
應(yīng)用層
這下我們終于可以放心大膽的發(fā)送消息了,PCT協(xié)議是個(gè)負(fù)責(zé)任的協(xié)議,如果能送到,他就一定 會(huì)送到,并且是有序的,要是網(wǎng)絡(luò)壞掉了,實(shí)在連不上,他就會(huì)告訴我網(wǎng)絡(luò)連不上。
這樣子來編程方便多了呀。
現(xiàn)在我想知道瀏覽器和服務(wù)器是怎么通信的。我們來看看百度。
- $ 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 之后回車,百度就給我返回了下面的一長串,然后瀏覽器再根據(jù) 返回的內(nèi)容進(jìn)行渲染,這又是一個(gè)大話題了,不講了不講了,收工 :)