一、網(wǎng)絡(luò)發(fā)展歷史
互聯(lián)網(wǎng)從何而來?
這要追溯到上個(gè)世紀(jì) 50 - 60 年代,當(dāng)時(shí)正逢美蘇爭霸冷戰(zhàn),核武器給戰(zhàn)爭雙方提供了足夠的威懾力,想要保全自己,就要保證自己的反制手段是有效的。
如何保證能夠反擊:
- 保存指揮機(jī)構(gòu)
- 保存核彈頭和發(fā)射井
- 指揮機(jī)構(gòu)和核彈頭之間的通信鏈路
需要保證通信鏈路在核彈洗地的情況下仍然能正常運(yùn)作
最終方案,以力破巧!讓指揮機(jī)構(gòu)和核彈頭之間,有無數(shù)條可以通信的鏈路,哪怕其中一部分被打掉了,剩余的仍然能夠正常工作,從而衍生出了今天的互聯(lián)網(wǎng)。中國互聯(lián)網(wǎng)的發(fā)展是非常滯后的,90年代左右,國內(nèi)的計(jì)算機(jī)才逐漸多了起來,隨著計(jì)算機(jī)和網(wǎng)絡(luò)的普及,中國這個(gè)十億級(jí)別的市場開始爆發(fā)整個(gè)互聯(lián)網(wǎng)行業(yè)出現(xiàn)井噴式發(fā)展。
2007年,國外出現(xiàn)了一件驚天動(dòng)地的大事,喬布斯發(fā)布了第一代蘋果手機(jī) lPhone,手機(jī)從功能機(jī)向智能機(jī)轉(zhuǎn)變。
智能手機(jī)對(duì)于國內(nèi)的影響,其實(shí)大概是2012年左右才開始,這個(gè)時(shí)候國內(nèi)的智能手機(jī)才逐漸普及。
2012年以后,互聯(lián)網(wǎng)行業(yè)迎來了第二波發(fā)展高峰:移動(dòng)互聯(lián)網(wǎng)。
二、網(wǎng)絡(luò)通信基礎(chǔ)
1、局域網(wǎng) / 廣域網(wǎng)
Local Area Network,簡稱LAN。
Local 即標(biāo)識(shí)了局域網(wǎng)是本地,局部組建的一種私有網(wǎng)絡(luò)局域網(wǎng)內(nèi)的主機(jī)之間能方便的進(jìn)行網(wǎng)絡(luò)通信,又稱為內(nèi)網(wǎng);局域網(wǎng)和局域網(wǎng)之間在沒有連接的情況下,是無法通信的。
兩根線把三個(gè)主機(jī)給連起來,這三個(gè)主機(jī)就構(gòu)成了一個(gè)局域網(wǎng)。
局域網(wǎng)組建網(wǎng)絡(luò)的方式有很多種,咱們?nèi)粘J褂玫碾娔X一般都是一個(gè)網(wǎng)口,但是也有的主機(jī)是帶有多個(gè)網(wǎng)口的,這種組網(wǎng)方式是非常少見 (非常費(fèi)網(wǎng)線,也非常費(fèi)網(wǎng)口)
一般組件局域網(wǎng),都會(huì)使用一些轉(zhuǎn)發(fā)設(shè)備:交換機(jī),路由器
-
交換機(jī)
- 借助交換機(jī),就組成了一個(gè)局域網(wǎng),交換機(jī)上面的網(wǎng)口之間都是對(duì)等 (都是─樣的口)
效果就是把插在上面的設(shè)備給組建成一個(gè)局域網(wǎng),這個(gè)局域網(wǎng)內(nèi)部的主機(jī)之間就可以相互進(jìn)行訪問 - 交換機(jī)是把若干個(gè)設(shè)備給組建到一個(gè)局域網(wǎng)中
- 借助交換機(jī),就組成了一個(gè)局域網(wǎng),交換機(jī)上面的網(wǎng)口之間都是對(duì)等 (都是─樣的口)
-
路由器
- 這個(gè)是咱們?nèi)粘V凶畛R姷那闆r。路由器這里其實(shí)有兩類端口,
WAN 口
LAN 口
其中插在 LAN 口上的設(shè)備,在一個(gè)局域網(wǎng)里,通過 wan 口連接到另外一個(gè)局域網(wǎng) - 路由器則是連接了兩個(gè)局域網(wǎng) (LAN口是一個(gè),WAN又連了一個(gè))
- 這個(gè)是咱們?nèi)粘V凶畛R姷那闆r。路由器這里其實(shí)有兩類端口,
-
集線器:
- 實(shí)際上基本沒有使用集線器組網(wǎng)的,集線器相當(dāng)于把一根網(wǎng)線給分叉了
- 分出來的兩個(gè)叉不能一起用,用一個(gè)的時(shí)候另一個(gè)就不好使
上述討論的區(qū)別,局限于 "傳統(tǒng)”,的交換機(jī)和路由器。
實(shí)際上,真實(shí)的交換機(jī)和路由器之間的界限,已經(jīng)越來越模糊了,路由器的很多功能,交換機(jī)也有,交換機(jī)的很多功能,路由器也有
通過路由器 / 交換機(jī),組建起來的這些都叫做局域網(wǎng)。
廣域網(wǎng)其實(shí)和局域網(wǎng)之間,沒有明確界限。認(rèn)為比較大的局域網(wǎng),就可以稱為 “廣域網(wǎng)”。
全世界最大的廣域網(wǎng),叫做 Internet (因特網(wǎng))
2、IP地址 & 端口號(hào)
IP 地址:描述了網(wǎng)絡(luò)上的一個(gè)主機(jī)的位置 (收貨地址)
IP地址本質(zhì)上是一個(gè) 32 位的整數(shù),但是由于32位的整數(shù),不方便人來讀和記憶,一般常見的操作都是把這個(gè) 32 位的整數(shù),按照每個(gè)字節(jié),分成四個(gè)部分,中間用
.
分割,稱為 點(diǎn)分十進(jìn)制 。
例如:123.139.170.225,范圍是 0-255。
127.0.0.1 (一個(gè)特殊的IP地址,環(huán)回IP,表示自己這個(gè)主機(jī))
端口號(hào):描述了一個(gè)主機(jī)上的某個(gè)應(yīng)用程序 (收件人的電話)
端口號(hào)本質(zhì)上是一個(gè) 2 個(gè)字節(jié) (16位) 的無符號(hào)整數(shù),范圍 0-65535
例如:3306,MySQL 默認(rèn)的端口號(hào)
服務(wù)器程序在啟動(dòng)的時(shí)候,就需要綁定上一個(gè)端口號(hào),以便客戶端程序來訪問
3、協(xié)議
3.1、協(xié)議的概念
進(jìn)行有效的通信,前提就是能夠明確通信協(xié)議。本質(zhì)上就是約定,發(fā)出來的數(shù)據(jù)是什么的格式,接收方按照對(duì)應(yīng)的格式來進(jìn)行解析
網(wǎng)絡(luò)通信的時(shí)候,本質(zhì)上,傳輸?shù)氖枪庑盘?hào)和電信號(hào)
- 通過光信號(hào)的頻率 (高頻率 / 低頻率),電信號(hào)的電平 (高電平 / 低電平),來表示 0 和 1。
關(guān)于協(xié)議分層
網(wǎng)絡(luò)通信這個(gè)過程,其實(shí)很復(fù)雜,里面有很多很多的細(xì)節(jié),
如果就只通過一個(gè)協(xié)議,來約定所有的細(xì)節(jié),這個(gè)協(xié)議就會(huì)非常龐大,復(fù)雜,
更好的辦法,就是把一個(gè)大的復(fù)雜的協(xié)議,拆成多個(gè)小的,更簡單的協(xié)議,每個(gè)協(xié)議,負(fù)責(zé)一部分工作
(就和寫代碼一樣,寫一個(gè)復(fù)雜的程序,不能指望說,一個(gè)文件把所有的代碼都裝進(jìn)去,把這個(gè)代碼拆分成多個(gè)更小的,更簡單的文件,每個(gè)文件負(fù)責(zé)一部分工作)
- 好處1:每層協(xié)議不需要理解其他層協(xié)議的細(xì)節(jié) (更好的做到了封裝)
打電話的人,不需要理解電話的工作原理,就能完成打電話的操作,制造電話的人,也不需要稱為語言大師- 好處2:把對(duì)應(yīng)層的協(xié)議替換成其他協(xié)議 (更好的解耦合)
打電話的人,可以不使用有線電話,可以使用無線電話
打電話的人,也可以使用英語,不使用漢語
互聯(lián)網(wǎng)中的分層具體怎么分:
OSI 七層網(wǎng)絡(luò)模型
- 這種模型只是存在于教科書中,真實(shí)的情況是 OSI 的簡化版本:
TCP / IP 五層 (四層) 網(wǎng)絡(luò)模型
站在一個(gè)全局的角度,五層模型
站在純程序猿的角度,最下面的物理層描述的是硬件設(shè)備 (和軟件沒啥關(guān)系,和程序猿距離比較遠(yuǎn)) 這個(gè)時(shí)候就認(rèn)為是四層下面四層都是一樣的,這四層,和咱們程序猿的關(guān)系都不是很大,這里的代碼邏輯都是由操作系統(tǒng)和驅(qū)動(dòng)以及硬件已經(jīng)實(shí)現(xiàn)好的
程序猿打交道最多的,是這個(gè)應(yīng)用層的協(xié)議
3.2、TCP五層網(wǎng)絡(luò)模型
1、物理層: 網(wǎng)絡(luò)通信中的硬件設(shè)備
通信需要網(wǎng)線 / 網(wǎng)卡… 針對(duì)硬件設(shè)備的約定,就是物理層協(xié)議所負(fù)責(zé)的范疇,需要保證所有的主機(jī)和網(wǎng)絡(luò)設(shè)備之間,都是相互匹配的,隨便買一個(gè)路由器都可以插我的網(wǎng)線
2、數(shù)據(jù)鏈路層: 負(fù)責(zé)完成相鄰 (一根網(wǎng)線相連的兩個(gè)設(shè)備) 的兩個(gè)設(shè)備之間的通信的 [局部]
如果一個(gè)路由器連接了兩個(gè)主機(jī),路由器 和 主機(jī) 1 是相鄰的,路由器和主機(jī) 2 是相鄰的,主機(jī) 1 和主機(jī) 2 不是相鄰的
3、網(wǎng)絡(luò)層: 負(fù)責(zé)點(diǎn)到點(diǎn)之間的通信 [全局]
網(wǎng)絡(luò)中的任意節(jié)點(diǎn),到任意節(jié)點(diǎn)之間的通信 (不一定是相鄰了,更多的是指不相鄰的),網(wǎng)絡(luò)層就負(fù)責(zé)在這兩個(gè)點(diǎn)之間,規(guī)劃出一條合適的路線
實(shí)際的網(wǎng)絡(luò)環(huán)境結(jié)構(gòu)非常復(fù)雜,兩個(gè)點(diǎn)之間的路線不只一條,就需要規(guī)劃處最合適的一條 [高德地圖為你導(dǎo)航]
舉個(gè)例子: 從西安到吉林省白城市安廣鎮(zhèn),首先,規(guī)劃路線
1.西安 -> 北京 -> 白城 -> 安廣
2.西安 -> 長春 -> 白城 -> 安廣
3.西安 -> 沈陽 -> 白城 -> 安廣我就需要規(guī)劃哪一條路線最優(yōu) (最優(yōu)可能是指,時(shí)間最短,也可能是指成本最低,還可能是少換乘)
網(wǎng)絡(luò)層負(fù)責(zé)這個(gè)事情,網(wǎng)絡(luò)層允許用戶根據(jù)情況來決定哪種是 “最優(yōu)”。
假設(shè)我路線規(guī)劃好了
西安 -> 長春 -> 白城 -> 安廣
接下來就考慮具體如何實(shí)施,先考慮西安到長春,決定坐飛機(jī),到了長春了,再考慮如何到白城,決定坐火車,到白城了,考慮如何去安廣,決定坐大巴車,到了安廣,考慮如何到家里,決定坐毛驢車這個(gè)過程是數(shù)據(jù)鏈路層負(fù)責(zé)的工作
4、傳輸層: 負(fù)責(zé)端到端(起點(diǎn)和終點(diǎn)) 之間的通信
只是關(guān)注結(jié)果 (數(shù)據(jù)到?jīng)]到),不關(guān)注過程 (不關(guān)注數(shù)據(jù)是走哪條路,轉(zhuǎn)發(fā)的)
例如我網(wǎng)上購物,我就需要填寫自己的收件人地址和收件人姓名,商家就要根據(jù)這個(gè)地址把快遞發(fā)給我
我和商家,都是只關(guān)注結(jié)果,不關(guān)注過程
快遞公司,要關(guān)注中間的過程
5、應(yīng)用層: 和應(yīng)用程序密切相關(guān)的,你傳輸?shù)倪@個(gè)數(shù)據(jù),是跟什么用的
不同的應(yīng)用程序就有不同的用途
舉個(gè)例子:有一天我在網(wǎng)上買一個(gè)床刷子
商家,站在傳輸層,考慮這個(gè)東西是能不能發(fā)到我手上??爝f公司,站在網(wǎng)絡(luò)層規(guī)劃路線??爝f小哥,站在數(shù)據(jù)鏈路層,騎著電動(dòng)車把貨拉到集散中心。電動(dòng)車 / 集裝箱卡車 / 公路,站在物理層,提供傳輸?shù)幕A(chǔ)。
他們都是只在考慮包裹如何傳輸,不考慮這個(gè)包裹里面是什么,更不關(guān)心包裹里的東西的作用。但是我,作為買床刷子的人,就是抱著一定的用途 / 目的,來買的,這個(gè)是程序猿最最需要打交道的事情。
網(wǎng)絡(luò)設(shè)備所在分層 (傳統(tǒng)意義上的路由器和交換機(jī))
- 一臺(tái)主機(jī),其實(shí)就對(duì)應(yīng)了物理層到應(yīng)用層五層 (把這五層都給實(shí)現(xiàn)了)
- 一臺(tái)路由器,主要就是物理層到網(wǎng)絡(luò)層(主要是實(shí)現(xiàn)了物理層,數(shù)據(jù)鏈路層,網(wǎng)絡(luò)層)
- 一臺(tái)交換機(jī),主要就是物理層到數(shù)據(jù)鏈路層 (主要是實(shí)現(xiàn)了物理層,數(shù)據(jù)鏈路層)
4、封裝,分用
4.1、封裝
網(wǎng)絡(luò)分層中的一組重要概念,封裝和分用,(此處的 “封裝”,和 Java 面向?qū)ο螅胺庋b繼承多態(tài)” 的封裝,沒什么關(guān)系)
不同的分層的協(xié)議之間,是如何相互配合的
例如,使用 QQ 給一個(gè)同學(xué)發(fā)送消息,用戶 A 在鍵盤上輸入了一個(gè)"hello",按下發(fā)送鍵
應(yīng)用層 (QQ應(yīng)用程序)
- 根據(jù)用戶輸入的內(nèi)容,把數(shù)據(jù)構(gòu)造成一個(gè)應(yīng)用層的協(xié)議報(bào)文 (協(xié)議是一種約定,報(bào)文遵守了這個(gè)約定的一組數(shù)據(jù))
- QQ 的代碼中就會(huì)根據(jù)程序猿所設(shè)計(jì)的應(yīng)用層協(xié)議,來構(gòu)造出一個(gè) 應(yīng)用層的數(shù)據(jù)報(bào)文
- 這個(gè)協(xié)議長啥樣?都是程序猿自己約定的。QQ使用的應(yīng)用層協(xié)議,是開發(fā)QQ的程序猿約定的;LOL使用的應(yīng)用層協(xié)議,是開發(fā)LOL的程序猿;約定的淘寶使用的應(yīng)用層協(xié)議,是開發(fā)淘寶的程序猿約定的。顯然這些不同程序中使用的應(yīng)用層協(xié)議大概率是不相同的,QQ之外的人,是不知道 QQ 使用的應(yīng)用層協(xié)議是什么的。
- (其他的傳輸層、網(wǎng)絡(luò)層… 的協(xié)議都是現(xiàn)成,操作系統(tǒng) / 硬件 / 驅(qū)動(dòng)已經(jīng)實(shí)現(xiàn)好的),應(yīng)用層的協(xié)議大概率是程序猿自己設(shè)定的。
- 應(yīng)用層協(xié)議就調(diào)用操作系統(tǒng)提供的API (稱為socket API),把應(yīng)用層的數(shù)據(jù),交給傳輸層 (就已經(jīng)進(jìn)入操作系統(tǒng)內(nèi)核了)、
傳輸層 (操作系統(tǒng)內(nèi)核)
根據(jù)剛才傳過來的數(shù)據(jù),基于當(dāng)前使用的傳輸層協(xié)議,來構(gòu)造出一個(gè)傳輸層的協(xié)議報(bào)文
傳輸層最典型的協(xié)議:UDP,TCP。以 TCP 為例:
- 在應(yīng)用層數(shù)據(jù)的基礎(chǔ)上加上一個(gè) TCP 的協(xié)議報(bào)頭
也就是說 TCP 的數(shù)據(jù)報(bào) = TCP報(bào)頭+數(shù)據(jù)載荷 (Payload,也就是一個(gè)完整的應(yīng)用層數(shù)據(jù))
- 可以簡單的把這個(gè)構(gòu)造 TCP 報(bào)文的過程視為是一個(gè)字符串拼接 (這里拼的是二進(jìn)制數(shù)據(jù))
- TCP的報(bào)頭中有很多信息
其中最重要的,就是 “源端口” 和 “目的端口”,也就是發(fā)件人電話和收件人電話- 應(yīng)用層和傳輸層的過程就是封裝,類似于快遞打包。
打包的目的,一方面是為了保護(hù)衣服,不被弄壞弄臟;另一方面,是為了往上面貼標(biāo)簽,標(biāo)簽上就有轉(zhuǎn)發(fā)數(shù)據(jù)的重要輔助信息。
網(wǎng)絡(luò)中的封裝,不需要考慮 “數(shù)據(jù)弄壞弄臟的問題”。這里主要的目的就是為了“貼標(biāo)簽",貼上輔助轉(zhuǎn)發(fā)的信息。- 接下來就會(huì)把這個(gè)傳輸層的數(shù)據(jù)報(bào),交給網(wǎng)絡(luò)層
網(wǎng)絡(luò)層 (操作系統(tǒng)內(nèi)核)
- 拿到了完整的傳輸層數(shù)據(jù)報(bào),就會(huì)再根據(jù)當(dāng)前使用的網(wǎng)絡(luò)層協(xié)議 (例如IP),再次進(jìn)行封裝,把 TCP 數(shù)據(jù)報(bào)構(gòu)造成 IP 數(shù)據(jù)報(bào),還是添加上一個(gè) IP 協(xié)議報(bào)頭
IР 數(shù)據(jù)報(bào) = IP 協(xié)議報(bào)頭+載荷 (完整的 TCP / UDP 的數(shù)據(jù)報(bào))- 這個(gè)報(bào)頭中也有很多重要的信息
其中最重要的就是 源IP 和 目的IP,相當(dāng)于發(fā)件人的地址,和收件人的地址
緊接著,當(dāng)前的網(wǎng)絡(luò)層協(xié)議,就會(huì)把這個(gè) IР數(shù)據(jù)報(bào),交給數(shù)據(jù)鏈路層
數(shù)據(jù)鏈路層 (驅(qū)動(dòng)程序)
在剛才的 IP數(shù)據(jù)報(bào)基礎(chǔ)上,根據(jù)當(dāng)前使用的數(shù)據(jù)鏈路層的協(xié)議,給構(gòu)造成一個(gè) 數(shù)據(jù)鏈路層的數(shù)據(jù)報(bào) ,就是加上幀頭和幀尾。
典型的數(shù)據(jù)鏈路層的協(xié)議,叫做 “以太網(wǎng)”,就會(huì)構(gòu)造成一個(gè) “以太網(wǎng)數(shù)據(jù)幀”。
以太網(wǎng)數(shù)據(jù)幀 = 幀頭+I(xiàn)P數(shù)據(jù)報(bào)+幀尾幀頭里也有很都重要的信息
最重要的信息,接下來要傳給的設(shè)備的地址是什么IР協(xié)議 里面寫的地址,是起點(diǎn)和終點(diǎn) (西安和安廣鎮(zhèn))
以太網(wǎng)數(shù)據(jù)幀,幀頭里,寫的地址,是接下來一個(gè)相鄰節(jié)點(diǎn)的地址 (西安和長春),隨著數(shù)據(jù)往下一個(gè)設(shè)備轉(zhuǎn)發(fā),幀頭中的地址,一直在時(shí)刻發(fā)生改變。
我人在西安,這里的地址,寫的是西安 / 長春
我人在長春,這里的地址,寫的是長春 / 白城
我人在白城,這里的地址,寫的是白城 / 安廣數(shù)據(jù)鏈路層,又會(huì)把這個(gè)數(shù)據(jù)交個(gè)物理層
物理層 (硬件設(shè)備)
- 做的工作就是,根據(jù)剛才的以太網(wǎng)數(shù)據(jù)幀 (其實(shí)就是一組 0 1 ),把這里的 0 1 變成高低電平,通過網(wǎng)線傳輸出去?;蛘甙堰@里的 0 1 變成高頻 / 低頻的電磁波,通過光纖 / 無線的方式傳播出去。
以上都是封裝,從上往下,就是數(shù)據(jù)從上層協(xié)議,交給下層協(xié)議,由下層協(xié)議進(jìn)行封裝 (構(gòu)造成該層協(xié)議的報(bào)文)
4.2、分用
到了剛才這一步,此時(shí)數(shù)據(jù)就已經(jīng)離開了當(dāng)前主機(jī),前往了下一個(gè)設(shè)備,下一個(gè)設(shè)備可能是路由器 / 交換機(jī) / 其他設(shè)備
A 和 B 之間,大概率不是網(wǎng)線直連的,中間就有很多個(gè)路由器和交換機(jī)來負(fù)責(zé)數(shù)據(jù)的轉(zhuǎn)發(fā)
中間的過程暫且不表,主要先看,數(shù)據(jù)到達(dá) B 之后的表現(xiàn)
物理層 (硬件設(shè)備,網(wǎng)卡)
- 主機(jī) B 的網(wǎng)卡感知到了一組高低電平,然后就會(huì)把這些電平翻譯成 0 1 的一串?dāng)?shù)據(jù),然后這一串 0 1 就是一個(gè)完整的以太網(wǎng)數(shù)據(jù)幀
物理層就把這個(gè)數(shù)據(jù)往上交給了數(shù)據(jù)鏈路層
數(shù)據(jù)鏈路層 (驅(qū)動(dòng)程序)
- 數(shù)據(jù)鏈路層負(fù)責(zé)對(duì)這個(gè)數(shù)據(jù)進(jìn)行解析,去掉幀頭和幀尾,
取出里面的 IP數(shù)據(jù)報(bào),然后交給網(wǎng)絡(luò)層協(xié)議
網(wǎng)絡(luò)層 (操作系統(tǒng)內(nèi)核)
- 網(wǎng)絡(luò)層協(xié)議 (IP 協(xié)議) 又會(huì)對(duì)這個(gè)數(shù)據(jù)進(jìn)行解析,去掉 IP 協(xié)議報(bào)頭
取出里面的 TCP 數(shù)據(jù)報(bào)再交給傳輸層
傳輸層 (操作系統(tǒng)內(nèi)核)
- 傳輸層協(xié)議 (TCP 協(xié)議) 又會(huì)對(duì)這個(gè)數(shù)據(jù)進(jìn)行解析,去掉 TCP 報(bào)頭,
取出里面的 TCP 數(shù)據(jù)報(bào),交給應(yīng)用層(QQ)
應(yīng)用層 (應(yīng)用程序,QQ)
- 應(yīng)用層就會(huì)調(diào)用 socket API,從內(nèi)核中讀取到這個(gè)應(yīng)用層數(shù)據(jù)報(bào),再按照應(yīng)用層協(xié)議進(jìn)行解析
根據(jù)解析結(jié)果給顯示到窗口中心
以上是分用,分用就是封裝的逆過程
封裝是從上往下,數(shù)據(jù)依次被加上了協(xié)議報(bào)頭 (包快遞)
分用是從下往上,數(shù)據(jù)一次被去掉了協(xié)議報(bào)頭 (拆快遞)
上述討論的只是起點(diǎn)和終點(diǎn)的情況,A 和 B 中間還有很多路由器和交換機(jī)
交換機(jī)先分用數(shù)據(jù)解析到數(shù)據(jù)鏈路層,更新以太網(wǎng)數(shù)據(jù)幀的幀頭里的地址,然后再重新封裝,并進(jìn)行轉(zhuǎn)發(fā)
路由器先分用數(shù)據(jù)到網(wǎng)絡(luò)層,拿到 IP 地址之后,進(jìn)行下一階段的路徑規(guī)劃,然后重新往下封裝,并進(jìn)行轉(zhuǎn)發(fā)
A 和 B 之間有多少個(gè)路由器或交換機(jī),無論網(wǎng)絡(luò)多么復(fù)雜,這里整體的傳輸過程都是類似的,只是在不停地重復(fù)封裝和分用的過程
三、網(wǎng)絡(luò)編程基本概念
為什么需要網(wǎng)絡(luò)編程:
用戶在瀏覽器中,打開在線視頻網(wǎng)站,如優(yōu)酷看視頻,實(shí)質(zhì)是通過網(wǎng)絡(luò),獲取到網(wǎng)絡(luò)上的一個(gè)視頻資源
與本地打開視頻文件類似,只是視頻文件這個(gè)資源的來源是網(wǎng)絡(luò)。
相比本地資源來說,網(wǎng)絡(luò)提供了更為豐富的網(wǎng)絡(luò)資源
網(wǎng)絡(luò)資源:所謂的網(wǎng)絡(luò)資源,其實(shí)就是在網(wǎng)絡(luò)中可以獲取的各種數(shù)據(jù)資源。
而所有的網(wǎng)絡(luò)資源,都是通過網(wǎng)絡(luò)編程來進(jìn)行數(shù)據(jù)傳輸?shù)摹?/p>
網(wǎng)絡(luò)編程,指網(wǎng)絡(luò)上的主機(jī),通過不同的進(jìn)程,以編程的方式實(shí)現(xiàn)網(wǎng)絡(luò)通信(或稱為網(wǎng)絡(luò)數(shù)據(jù)傳輸)
當(dāng)然,我們只要滿足進(jìn)程不同就行;所以即便是同一個(gè)主機(jī),只要是不同進(jìn)程,基于網(wǎng)絡(luò)來傳輸數(shù)據(jù),也屬于網(wǎng)絡(luò)編程。
特殊的,對(duì)于開發(fā)來說,在條件有限的情況下,一般也都是在一個(gè)主機(jī)中運(yùn)行多個(gè)進(jìn)程來完成網(wǎng)絡(luò)編程。
但是,我們一定要明確,我們的目的是提供網(wǎng)絡(luò)上不同主機(jī),基于網(wǎng)絡(luò)來傳輸數(shù)據(jù)資源:
- 進(jìn)程A:編程來獲取網(wǎng)絡(luò)資源
- 進(jìn)程B:編程來提供網(wǎng)絡(luò)資源
發(fā)送端和接收端:
在一次網(wǎng)絡(luò)數(shù)據(jù)傳輸時(shí):
發(fā)送端:數(shù)據(jù)的發(fā)送方進(jìn)程,稱為發(fā)送端。發(fā)送端主機(jī)即網(wǎng)絡(luò)通信中的源主機(jī)。
接收端:數(shù)據(jù)的接收方進(jìn)程,稱為接收端。接收端主機(jī)即網(wǎng)絡(luò)通信中的目的主機(jī)。
**收發(fā)端:**發(fā)送端和接收端兩端,也簡稱為收發(fā)端。
注意:發(fā)送端和接收端只是相對(duì)的,只是一次網(wǎng)絡(luò)數(shù)據(jù)傳輸產(chǎn)生數(shù)據(jù)流向后的概念
請(qǐng)求和響應(yīng):
一般來說,獲取一個(gè)網(wǎng)絡(luò)資源,涉及到兩次網(wǎng)絡(luò)數(shù)據(jù)傳輸:
第一次:請(qǐng)求數(shù)據(jù)的發(fā)送
第二次:響應(yīng)數(shù)據(jù)的發(fā)送
好比在快餐店點(diǎn)一份炒飯:
先要發(fā)起請(qǐng)求:點(diǎn)一份炒飯,再有快餐店提供的對(duì)應(yīng)響應(yīng):提供一份炒飯
客戶端和服務(wù)端:
服務(wù)端:在常見的網(wǎng)絡(luò)數(shù)據(jù)傳輸場景下,把提供服務(wù)的一方進(jìn)程,稱為服務(wù)端,可以提供對(duì)外服務(wù)。
客戶端:獲取服務(wù)的一方進(jìn)程,稱為客戶端。
對(duì)于服務(wù)來說,一般是提供:
- 客戶端獲取服務(wù)資源
- 客戶端保存資源在服務(wù)端
好比在銀行辦事:
銀行提供存款服務(wù):用戶(客戶端)保存資源(現(xiàn)金)在銀行(服務(wù)端)
銀行提供取款服務(wù):用戶(客戶端)獲取服務(wù)端資源(銀行替用戶保管的現(xiàn)金)
常見的客戶端服務(wù)端模型:
最常見的場景,客戶端是指給用戶使用的程序,服務(wù)端是提供用戶服務(wù)的程序:
客戶端先發(fā)送請(qǐng)求到服務(wù)端
服務(wù)端根據(jù)請(qǐng)求數(shù)據(jù),執(zhí)行相應(yīng)的業(yè)務(wù)處理
服務(wù)端返回響應(yīng):發(fā)送業(yè)務(wù)處理結(jié)果
客戶端根據(jù)響應(yīng)數(shù)據(jù),展示處理結(jié)果(展示獲取的資源,或提示保存資源的處理結(jié)果)
四、網(wǎng)絡(luò)編程套接字
Socket套接字,是由系統(tǒng)提供用于網(wǎng)絡(luò)通信的技術(shù),是基于 TCP / IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元?;?Socket套接字的網(wǎng)絡(luò)程序開發(fā)就是網(wǎng)絡(luò)編程。
Socket 套接字主要針對(duì)傳輸層協(xié)議劃分為如下三類:
流套接字:使用傳輸層 TCP 協(xié)議,TCP,即Transmission Control Protocol(傳輸控制協(xié)議),傳輸層協(xié)議
數(shù)據(jù)報(bào)套接字:使用傳輸層 UDP 協(xié)議
UDP,即User Datagram Protocol(用戶數(shù)據(jù)報(bào)協(xié)議),傳輸層協(xié)議。原始套接字:原始套接字用于自定義傳輸層協(xié)議,用于讀寫內(nèi)核沒有處理的IP協(xié)議數(shù)據(jù)。
我們不學(xué)習(xí)原始套接字,簡單了解即可。
1、TCP / UDP
網(wǎng)絡(luò)編程套接字,是操作系統(tǒng)給應(yīng)用程序提供的一組 API (叫做 socket API) socket 原義插座
socket 可以視為是應(yīng)用層和傳輸層之間的通信橋梁,
傳輸層 的核心協(xié)議有兩種:TCP,UDP
socket API 也有對(duì)應(yīng)的兩組,由于 TCP 和 UDP 協(xié)議差別很大,因此,這兩組 API 差別也很大
- TCP:有連接,可靠傳輸,面向字節(jié)流,全雙工
- UDP:無連接,不可靠傳輸,面向數(shù)據(jù)報(bào),全雙工
- 有連接:像打電話,得先接通,才能交互數(shù)據(jù)
- 無連接:像發(fā)微信,不需要接通,直接就能發(fā)數(shù)據(jù)
- 可靠傳輸:傳輸過程中,發(fā)送方知道接收方有沒有收到數(shù)據(jù)
打電話,就是可靠傳輸
阿里旺旺 / 釘釘 / 飛書已讀功能- 不可靠傳輸:傳輸過程中,發(fā)送方不知道接收方有沒有收到數(shù)據(jù)
發(fā)微信,就是不可靠傳輸錯(cuò)誤的理解:
可靠傳輸,就是數(shù)據(jù)發(fā)過去后 100% 能被對(duì)方收到 —— err
可靠傳輸,就是 “安全傳輸” —— err
- 面向字節(jié)流:以字節(jié)為單位進(jìn)行傳輸 (非常類似于文件操作中的字節(jié)流)
- 面向數(shù)據(jù)報(bào):以數(shù)據(jù)報(bào)為單位進(jìn)行傳輸 (一個(gè)數(shù)據(jù)報(bào)都會(huì)明確大小),一次發(fā)送 / 接收必須是一個(gè)完整的數(shù)據(jù)報(bào),不能是半個(gè),也不能是一個(gè)半
在代碼中體現(xiàn)地非常明顯
- 全雙工:一條鏈路,雙向通信
- 半雙工:一條鏈路,單向通信
TCP,UDP 都是全雙工
以上,是 TCP 和UDP 直觀上的區(qū)別,細(xì)節(jié)上還有很多很多的東西,后面再詳細(xì)介紹
五、UDP socket
UDP socket 中,主要涉及到兩個(gè)類
DatagramSocket
(Datagram:數(shù)據(jù)報(bào))這一個(gè) DatagramSocket 對(duì)象,就對(duì)應(yīng)到操作系統(tǒng)中的一個(gè) socket 文件
- 操作系統(tǒng)中的 “文件” 是一個(gè)廣義的概念。平時(shí)說的文件。只是指普通文件 (硬盤上的數(shù)據(jù))
實(shí)際上,操作系統(tǒng)中的文件還可能表示了一些硬件設(shè)備 / 軟件資源- socket 文件,就對(duì)應(yīng)這 "網(wǎng)卡” 這種硬件設(shè)備,從 socket 文件讀數(shù)據(jù),本質(zhì)上就是讀網(wǎng)卡,往 socket 文件寫數(shù)據(jù),本質(zhì)上就是寫網(wǎng)卡 。
你可以想象:socket 文件,就是一個(gè)遙控器,通過遙控器來操作網(wǎng)卡,這種行為非常常見,甚至早在三國時(shí)期,就有了董卓曹操,挾天子以令諸侯,天子就是這個(gè)天下的遙控器
DatagramPacket
代表了一個(gè) UDP 數(shù)據(jù)報(bào),使用 UDP 傳輸數(shù)據(jù)的基本單位。每次發(fā)送 / 接收數(shù)據(jù),都是在傳輸一個(gè) DatagramPacket 對(duì)象。
方法簽名 | 方法說明 |
---|---|
void receive(DatagramPacket p) | 從此套接字接收數(shù)據(jù)報(bào)(如果沒有接收到數(shù)據(jù)報(bào),該方法會(huì)阻 塞等待) |
void send(DatagramPacket p) | 從此套接字發(fā)送數(shù)據(jù)報(bào)包(不會(huì)阻塞等待,直接發(fā)送) |
void close() | 關(guān)閉此數(shù)據(jù)報(bào)套接字 |
1、UDP 回顯服務(wù)
1.1、服務(wù)器
寫一個(gè)最簡單的客戶端服務(wù)器程序,回顯服務(wù) EchoServer,這樣的程序?qū)儆谧詈唵蔚木W(wǎng)絡(luò)編程中的程序,不涉及到任何的業(yè)務(wù)邏輯,就只是通過 socket api 單純的轉(zhuǎn)發(fā)
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
端口號(hào):
此處在構(gòu)造服務(wù)器這邊的 socket 對(duì)象的時(shí)候就需要顯式的綁定一個(gè)端口號(hào)
端口號(hào)是用來區(qū)分一個(gè)應(yīng)用程序的,主機(jī)收到網(wǎng)卡上的數(shù)據(jù)的時(shí)候這個(gè)數(shù)據(jù)該給哪個(gè)程序?
port 在運(yùn)行程序的時(shí)候來指定即可端口號(hào)可以是自己定,也可以讓系統(tǒng)分配
當(dāng)前這個(gè)寫法,是自己定的,一會(huì)還能看到系統(tǒng)分配的SocketException:
構(gòu)造socket對(duì)象有很多失敗的可能
端口號(hào)已經(jīng)被占用了,兩個(gè)人不能有相同的電話號(hào)碼,同一個(gè)主機(jī)的兩個(gè)程序也不能有相同的端口號(hào)
多個(gè)進(jìn)程不能綁定同一個(gè)端口
一個(gè)進(jìn)程能不能綁定多個(gè)端口呢?可以的,一個(gè)人可以有多個(gè)手機(jī)號(hào)碼
一個(gè)進(jìn)程可以創(chuàng)建多個(gè) socket 對(duì)象,每個(gè) socket 對(duì)象都綁定自己的端口
如果一個(gè)程序需要使用網(wǎng)路通信,你至少得有一個(gè)端口。如果一個(gè)人需要網(wǎng)購,也得至少有一個(gè)電話號(hào)碼。每個(gè)進(jìn)程能夠打開的文件個(gè)數(shù),是有上限的。如果進(jìn)程之前已經(jīng)打開了很多很多的文件,就可能導(dǎo)致此處的 socket 文件就不能順利打開
為什么服務(wù)器第一步就是接收客戶端發(fā)來的請(qǐng)求,而不是發(fā)送呢?
因?yàn)椋?wù)器的定義,就是 “被動(dòng)接收請(qǐng)求” 的這一方。主動(dòng)發(fā)送請(qǐng)求的這一方面,叫做客戶端。
receive
方法是可能會(huì)阻塞的! 客戶端什么時(shí)候給服務(wù)器發(fā)請(qǐng)求?不確定的!
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
// 1、網(wǎng)絡(luò)編程,第一步就要準(zhǔn)備好 socket 實(shí)例,這是進(jìn)行網(wǎng)絡(luò)編程的大前提
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
// 此處在構(gòu)造服務(wù)器這邊的 socket 對(duì)象的時(shí)候就需要顯式的綁定一個(gè)端口號(hào)
// port 在運(yùn)行程序的時(shí)候來指定即可
socket = new DatagramSocket(port);
}
// 啟動(dòng)服務(wù)器
public void start() throws IOException {
System.out.println("啟動(dòng)服務(wù)器!");
// UDP 不需要建立連接,直接接收從客戶端來的數(shù)據(jù)即可
while (true) {
// 1、讀取客戶端發(fā)來的請(qǐng)求
// 為了接收數(shù)據(jù),需要先準(zhǔn)備好一個(gè)空的 DatagramPacket 對(duì)象,由 receive 進(jìn)行填充數(shù)據(jù)
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024); // 把一個(gè)字節(jié)數(shù)組包裝了
// 參數(shù)為 "輸出型參數(shù)"
socket.receive(requestPacket);
// 把 DatagramPacket 解析成一個(gè) String
// 假設(shè)此處的 UDP 數(shù)據(jù)報(bào)最長是 1024,這個(gè)長度不一定是 1024,實(shí)際的數(shù)據(jù)可能不夠 1024
String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
// 2、根據(jù)請(qǐng)求計(jì)算響應(yīng)(由于咱們這是一個(gè)回顯服務(wù),2省略)
String response = process(request);
// 3、把響應(yīng)寫回到客戶端
// send 方法的參數(shù),也是 DatagramPacket,需要把響應(yīng)數(shù)據(jù)先構(gòu)造成一個(gè) DatagramPacket 再進(jìn)行發(fā)送,這里就不是構(gòu)造一個(gè)空的數(shù)據(jù)報(bào)
// 這里的參數(shù)不再是一個(gè)空的字節(jié)數(shù)組了,response 是剛才根據(jù)請(qǐng)求計(jì)算得到的響應(yīng),非空的 DatagramPacket 里面的數(shù)據(jù)就是String response的數(shù)據(jù)
// 寫成 response.length() 表示(字符的個(gè)數(shù))。 這里拿到的是字節(jié)數(shù)組的長度(字節(jié)的個(gè)數(shù))
/*如果代碼光是這么寫,還是不太行,此時(shí)就無法區(qū)分出,這個(gè)數(shù)據(jù)要交給誰了
在發(fā)送數(shù)據(jù)的時(shí)候,必須要指定,這個(gè)數(shù)據(jù)報(bào)發(fā)給誰?地址 + 電話
lP + port
在當(dāng)前的場景中,哪個(gè)客戶端發(fā)來的請(qǐng)求,就把數(shù)據(jù)返回給哪個(gè)客戶端
進(jìn)之后的版本,在 DatagramPacket構(gòu)造方法中,指定了第三個(gè)參數(shù),表示要把數(shù)據(jù)發(fā)給哪個(gè)地址 + 端口
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length);*/
// 改進(jìn):
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress()); // SocketAddress 就可以視為是一個(gè)類,里面包含了 IP 和端口
socket.send(responsePacket);
System.out.printf("[%s : %d] req: %s, req: %s\n",
requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
// 由于是回顯服務(wù),響應(yīng)和請(qǐng)求一樣
// 實(shí)際上對(duì)于一個(gè)真實(shí)的服務(wù)器來說,這個(gè)過程是最復(fù)雜的,為了實(shí)現(xiàn)這個(gè)過程,可能需要幾萬,幾十萬代碼
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
1.2、客戶端
指定端口號(hào)?
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port); // 自己指定
}
public UdpEchoClient() throws SocketException {
socket = new DatagramSocket(); // 系統(tǒng)隨機(jī)分配
}
第一個(gè)就是你去營業(yè)廳辦理電話卡,自己手動(dòng)挑一個(gè)喜歡的號(hào)碼
在客戶端構(gòu)造 socket 對(duì)象的時(shí)候,就不再手動(dòng)指定端口號(hào),使用無參版本的構(gòu)造方法
不指定端口號(hào),意思是,讓操作系統(tǒng)自己分配一個(gè)空閑的端口號(hào)
這個(gè)操作就是辦電話卡,對(duì)于號(hào)碼無感,人家給你隨機(jī)指定一個(gè)號(hào)碼通常寫代碼的時(shí)候,服務(wù)器都是手動(dòng)指定的,客戶端都是由系統(tǒng)自動(dòng)指定的 (系統(tǒng)隨機(jī)分配一個(gè))
對(duì)于服務(wù)器來說,必須要手動(dòng)指定,后續(xù)客戶端要根據(jù)這個(gè)端口來訪問到服務(wù)器
如果讓系統(tǒng)隨機(jī)分配,客戶端就不知道服務(wù)器的端口是啥,不能訪問,對(duì)于客戶端來說,如果手動(dòng)指定,也行,但是系統(tǒng)隨機(jī)分配更好
一個(gè)機(jī)器上的兩個(gè)進(jìn)程,不能綁定同一個(gè)端口
客戶端就是普通用戶的電腦,天知道用戶電腦上都裝了什么程序,天知道用戶的電腦上已經(jīng)被占用了哪些端口,如果你手動(dòng)指定一個(gè)端口,萬一這個(gè)端口被別的程序占用,咱們的程序不就不能正常工作了嘛?
而且由于客戶端是主動(dòng)發(fā)起請(qǐng)求的一方,客戶端需要在發(fā)送請(qǐng)求之前,先知道服務(wù)器的地址 + 端口,但是反過來在請(qǐng)求發(fā)出去之前,服務(wù)器是不需要事先知道客戶端的地址 + 端口
構(gòu)造方法:
// 法是只構(gòu)造了保存數(shù)據(jù)的空間,沒有數(shù)據(jù)內(nèi)容,也沒有地址~[用于接收]
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
// 這種寫法,也是,既構(gòu)造了數(shù)據(jù),有能構(gòu)造目標(biāo)地址.這個(gè)目標(biāo)地址, IP和端口是合在一起的寫法. (InetSocketAddress)[用于發(fā)送]
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
// 又使用到了一種DatagramPacket構(gòu)造方法.既能構(gòu)造數(shù)據(jù),又能構(gòu)造目標(biāo)地址.這個(gè)目標(biāo)地址是IP和端口分開的寫法~~[用于發(fā)送)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName("127.0.0.1"), 9090);
五元組:
寫代碼的時(shí)候,就會(huì)涉及到一系列的 ip 和 端口。
一次通信,是由五個(gè)核心信息,描述出來的。源 IP,源端口,目的IP,目的端口,協(xié)議類型。站在服務(wù)器的角度:
- 源IP:服務(wù)器程序本機(jī)的 IP
- 源端口:服務(wù)器綁定的端口 (此處手動(dòng)指定了 9090)
- 目的 IP:包含在收到的數(shù)據(jù)報(bào)中 (客戶端的 IP)
- 目的端口:包含在收到的數(shù)據(jù)報(bào)中 (客戶端的端口)
- 協(xié)議類型:UDP
站在客戶端的角度:
- 源IP:本機(jī) IP
- 源端口:系統(tǒng)分配的端口
- 目的IP:服務(wù)器的 IP
- 目的端口:服務(wù)器的端口
- 協(xié)議類型:UDP
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String ip, int port) throws SocketException {
socket = new DatagramSocket();
this.serverIP = ip;
this.serverPort = port;
// 此處的 port 是服務(wù)器的端口,客戶端啟動(dòng)的時(shí)候,不需要給 socket 指定端口,客戶端自己的端口是系統(tǒng)隨機(jī)分配的
}
// 在客戶端構(gòu)造 socket 對(duì)象的時(shí)候,就不再手動(dòng)指定端口號(hào),使用無參版本的構(gòu)造方法
/*public UdpEchoClient() throws SocketException {
socket = new DatagramSocket();
}*/
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1、先從控制臺(tái)讀取用戶輸入的字符串
System.out.print("-> ");
String request = scanner.next();
// 2、把這個(gè)用戶輸入的內(nèi)容,構(gòu)造成一個(gè) UDP 請(qǐng)求,并發(fā)送
// 構(gòu)造的請(qǐng)求里包含兩部分信息
// 1) 數(shù)據(jù)的內(nèi)容:request 字符串
// 2) 數(shù)據(jù)要發(fā)給誰:服務(wù)器的 IP + 端口號(hào)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3、從服務(wù)器讀取響應(yīng)數(shù)據(jù),并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
// 4、把響應(yīng)結(jié)果顯示到控制臺(tái)上
System.out.printf("request: %s, response: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
// 由于客戶端和服務(wù)器在同一個(gè)機(jī)器上,使用的 IP 仍是 127.0.0.1,如果是不同的機(jī)器,就要修改這里的 IP
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
早就已經(jīng)把服務(wù)器啟動(dòng)起來了,啟動(dòng)了服務(wù)器之后,才開始寫客戶端代碼的,在寫客戶端代碼的這個(gè)過程中,顯然,沒人訪問服務(wù)器的,
服務(wù)器其實(shí)就卡在 receive 這里,阻塞等待了。
啟動(dòng)服務(wù)器!
[/127.0.0.1 : 63140] request: hello, response: hello
63140:這個(gè)就是系統(tǒng)自動(dòng)給客戶端分配的端口
客戶端是可以有很多的
一個(gè)服務(wù)器可以給很多很多客戶端提供服務(wù),一個(gè)餐館,可以給很多很多的客人提供就餐服務(wù)的取決于服務(wù)器的能力,同一時(shí)刻服務(wù)器能夠處理的客戶端的數(shù)目存在上限的,
服務(wù)器處理每個(gè)請(qǐng)求,都需要消耗一定的硬件資源 (包括不限于,CPU,內(nèi)存,磁盤,帶寬…)
能處理多少客戶端,取決于:
處理一個(gè)請(qǐng)求,消耗多少資源
機(jī)器一共有多少資源能用
(在 Java 中并不容易精確的計(jì)算消耗多少資源,,JVM 里面有很多輔助性的功能,也要消耗額外的資源)
實(shí)際開發(fā)中,通過性能測試的方式,就知道了能有多少個(gè)客戶端
問題:
當(dāng)我們像再啟動(dòng)一個(gè)客戶端的時(shí)候,遇到了點(diǎn)小困難,idea 提示咱們要把上一個(gè)客戶端給干掉
‘UdpEchoClient’ is not allowed to run in parallel.
Would you like to stop the running one?IDEA 中默認(rèn)情況下,一個(gè)程序只能啟動(dòng)一個(gè)實(shí)例.再次啟動(dòng)就會(huì)干掉之前的實(shí)例,此處勾選上這個(gè)選項(xiàng),就可以啟動(dòng)多個(gè)實(shí)例了
[/127.0.0.1y.S0368yreq: hello, resp: hello
[/127.0.e.1: 598o2]req: java,resp: java
每個(gè)客戶端,都被系統(tǒng)分配了不同的端口:通常情況下,一個(gè)服務(wù)器,是要同時(shí)給多個(gè)客戶端提供服務(wù)的
但是也有情況,就是一個(gè)服務(wù)器只給一個(gè)客戶端提供服務(wù) (典型就是在分布式系統(tǒng)中,兩個(gè)節(jié)點(diǎn)之間的交互)
上述寫的代碼雖然只是針對(duì)一個(gè)簡單的回顯服務(wù),但是對(duì)于一個(gè)復(fù)雜的服務(wù)器來說,做的工作的基本流程,也是類似的
2、UDP 翻譯
再來寫一個(gè)簡單程序,帶上點(diǎn)業(yè)務(wù)邏輯,寫一個(gè)翻譯程序 (英譯漢)
請(qǐng)求是一些簡單的英文單詞,響應(yīng)也就是英文單詞對(duì)應(yīng)的翻譯
客戶端不變,把服務(wù)器代碼進(jìn)行調(diào)整
主要是調(diào)整process
方法
讀取請(qǐng)求并解析,把響應(yīng)寫回給客戶端,這倆步驟都一樣,關(guān)鍵的邏輯就是 “根據(jù)請(qǐng)求處理響應(yīng)”
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
public HashMap<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 簡單構(gòu)造幾個(gè)詞
dict.put("cat", "貓");
dict.put("dog", "狗");
dict.put("pig", "豬");
}
@Override
public String process(String request) {
// UdpEchoServer 中的 process 改成 public
return dict.getOrDefault(request, "該詞無法被翻譯!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
六、TCP
1、TCP 回顯服務(wù)
1.1、服務(wù)器
TCP api 中,也是涉及到兩個(gè)核心的類
ServerSocket
(專門給 TCP 服務(wù)器用的)Socket
(既需要給服務(wù)器用,又需要給客戶端用)主要通過這樣的類,來描述一個(gè) socket 文件即可,而不需要專門的類表示 “傳輸?shù)陌?,面向字?jié)流,以字節(jié)為單位傳輸?shù)?/p>
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
// listen 英文原意:監(jiān)聽。但是 Java socket 中體現(xiàn)出 "監(jiān)聽" 的含義,
// 這樣叫是因?yàn)?,操作系統(tǒng)原生的 API 中,有一個(gè)操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務(wù)器啟動(dòng)了!");
while (true) {
// 1、建立連接
// 由于 TCP 是有連接的,不能一上來就讀數(shù)據(jù),需要先建立連接 (接電話)
// accept 就是在接電話,接電話的前提是,有人給你打,如果當(dāng)前客戶端嘗試建立連接,此處的 accept 就會(huì)阻塞
// accept 返回了一個(gè) socket 對(duì)象,稱為 clientSocket,后續(xù)和客戶端之間的溝通,都是都過 clientSocket 來完成的
Socket clientSocket = serverSocket.accept();
// 2、處理連接
// 這里之所分成了兩步 就是因?yàn)橐⑦B接 一個(gè)專門負(fù)責(zé)建立連接 一個(gè)專門負(fù)責(zé)數(shù)據(jù)通信
processConnection(clientSocket);
}
}
// 處理連接
private void processConnection(Socket clientSocket) {
System.out.printf("[%s : %d] 客戶端建立連接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下來處理請(qǐng)求和響應(yīng)
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 把 inputStream 中的數(shù)據(jù)讀出來,寫入到 outputStream 中
// 循環(huán)地處理每個(gè)請(qǐng)求,分別返回響應(yīng)
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1、讀取請(qǐng)求
if (!scanner.hasNext()) {
System.out.printf("[%s : %d] 客戶端斷開連接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
String request = scanner.next(); // 此處用 Scanner 更方便,如果用 InputStream 的 read 也可以
// 2、根據(jù)請(qǐng)求,計(jì)算響應(yīng)
String response = process(request);
// 3、將這個(gè)響應(yīng)返回客戶端
// 方便起見,用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新,如果沒有這個(gè)刷新,可能客戶端就不能第一時(shí)間看到響應(yīng)結(jié)果
printWriter.flush();
System.out.printf("[%s : %d] req : %s, resp: %s!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 記得關(guān)閉!
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
在上述代碼中,針對(duì)這里的 clientSocket
特意關(guān)閉了一下,但是對(duì)于 ServerSocket
就沒有關(guān)閉,同理 UDP 版本的代碼里,也沒有針對(duì) socket
的關(guān)閉,為什么?
關(guān)閉的目的是為了 “釋放資源” ,釋放資源的前提,是已經(jīng)不再使用這個(gè)資源了,
對(duì)于 UDP 的程序和 serversocket
來說,這些 socket
都是貫穿程序始終的,
這些資源最遲最遲,也就是跟隨進(jìn)程的退出一起釋放了 (進(jìn)程才是系統(tǒng)分配資源的基本單位)
而 clientSocket
這個(gè)是每個(gè)連接有一個(gè)的一,數(shù)目很多,連接斷開,也就不再需要了
每次都得保證處理完的連接都給進(jìn)行釋放
1.2、客戶端
對(duì)于 UDP 的
DatagramSocket
來說,構(gòu)造方法指定的端口,表示自己綁定哪個(gè)端口
對(duì)于 TCP 的ServerSocket
來說,構(gòu)造方法指定的端口,也是表示自己綁定哪個(gè)端口
對(duì)于 TCP 的Socket
來說,構(gòu)造方法指定的端口,表示要連接的服務(wù)器的端口,要和哪一個(gè)服務(wù)器上的哪一個(gè)端口建立連接
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
// 用普通的 socket 即可,不用 ServerSocket 了
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
// 這里可以給端口號(hào),但還這里給了之后,含義是不同的
// 傳入的 IP 和 端口號(hào) 的含義表示的不是自己綁定,而是服務(wù)器的,表示和這個(gè) IP 端口 建立連接!
// 調(diào)用這個(gè)構(gòu)造方法,就是和服務(wù)器建立連接 (打電話撥號(hào)了)
socket = new Socket(serverIP, serverPort);
}
public void start() {
System.out.println("和服務(wù)器連接成功");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream()) {
try (OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 仍是四個(gè)步驟
// 1、先從控制臺(tái)讀取用戶輸入的字符串
System.out.print("-> ");
String request = scanner.next();
// 2、把這個(gè)用戶輸入的內(nèi)容,構(gòu)造成一個(gè)請(qǐng)求,并發(fā)送
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush(); // 刷新,如果沒有這個(gè)刷新,可能客戶端就不能第一時(shí)間看到響應(yīng)結(jié)果
// 3、從服務(wù)器讀取響應(yīng)數(shù)據(jù),并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4、把響應(yīng)結(jié)果顯示到控制臺(tái)上
System.out.printf("req : %s, resp : %s\n", request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
運(yùn)行結(jié)果:
[/127.0.0.1 : 7085] 客戶端建立連接!
[/127.0.0.1 : 7085] req : test, resp: test!
[/127.0.0.1 : 7085] 客戶端斷開連接!
1.3、服務(wù)器 — 多線程
問題:
雖然此時(shí)的 TCP 代碼已經(jīng)跑起來了,但是此處還存在一個(gè)很嚴(yán)重的問題!
當(dāng)前的服務(wù)器,同一時(shí)刻,只能處理一個(gè)連接! [不科學(xué)]
為啥當(dāng)前咱們的服務(wù)器程序,只能處理一個(gè)客戶端?
能夠和客戶端交互的前提是,要先調(diào)用 accept,接收連接 (接通電話)上面的代碼,第一次
accept
結(jié)束之后,就會(huì)進(jìn)入processConnection
,在processConnection
又會(huì)有一個(gè)循環(huán)
如果 processConnection 里面的循環(huán)不結(jié)束,processConnection 就無法執(zhí)行完成
如果無法執(zhí)行完成,就導(dǎo)致外層循環(huán)無法進(jìn)入下一輪,也就無法第二次調(diào)用 accept , 也就不能接收第二個(gè)客戶端的連接了當(dāng)前這個(gè)問題,就好像你接了個(gè)電話,和對(duì)方你一言我一語的聊天,然后其他人再打電話,就沒法繼續(xù)接通了
解決:
要想解決上述問題,就得讓
processConnection
的執(zhí)行,和前面的 accept 的執(zhí)行互相不干擾,不能讓processConnection
里面的循環(huán)導(dǎo)致 accept 無法及時(shí)調(diào)用多線程!
問題:為啥咱們剛才 UDP 版本的程序就沒用多線程,也是好著的呀?
因?yàn)?UDP 不需要處理連接,UDP 只要一個(gè)循環(huán),就可以處理所有客戶端的請(qǐng)求
但是此處,TCP 既要處理連接,又要處理一個(gè)連接中的若干次請(qǐng)求,就需要兩個(gè)循環(huán),里層循環(huán),就會(huì)影響到外層循環(huán)的進(jìn)度了
- 主線程,循環(huán)調(diào)用
accept
,當(dāng)有客戶端連接上來的時(shí)候,就直接讓主線程創(chuàng)建一個(gè)新線程,由新線程負(fù)責(zé)對(duì)客戶端的若干個(gè)請(qǐng)求,提供服務(wù),(在新線程里,通過 while 循環(huán)來處理請(qǐng)求),這個(gè)時(shí)候,多個(gè)線程是并發(fā)執(zhí)行的關(guān)系 (宏觀上看起來同時(shí)執(zhí)行),就是各自執(zhí)行各自的了,就不會(huì)相互干擾
(也要注意,每個(gè)客戶端連上來都得分配一個(gè)線程)
只需要在剛剛的代碼中,改動(dòng) start()
的即可:
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
完整代碼:
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpThreadEchoServer {
// listen 英文原意:監(jiān)聽。但是 Java socket 中體現(xiàn)出 "監(jiān)聽" 的含義,
// 這樣叫是因?yàn)?,操作系統(tǒng)原生的 API 中,有一個(gè)操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務(wù)器啟動(dòng)了!");
while (true) {
// 1、建立連接
// 由于 TCP 是有連接的,不能一上來就讀數(shù)據(jù),需要先建立連接 (接電話)
// accept 就是在接電話,接電話的前提是,有人給你打,如果當(dāng)前客戶端嘗試建立連接,此處的 accept 就會(huì)阻塞
// accept 返回了一個(gè) socket 對(duì)象,稱為 clientSocket,后續(xù)和客戶端之間的溝通,都是都過 clientSocket 來完成的
Socket clientSocket = serverSocket.accept();
// [改進(jìn)方法] 在這里,每次 accept 成功,都創(chuàng)建一個(gè)新的線程,由新線程負(fù)責(zé)執(zhí)行這個(gè) processConnection 方法,串行變并發(fā)
Thread t = new Thread(() -> {
// 2、處理連接
// 這里之所分成了兩步 就是因?yàn)橐⑦B接 一個(gè)專門負(fù)責(zé)建立連接 一個(gè)專門負(fù)責(zé)數(shù)據(jù)通信
processConnection(clientSocket);
});
t.start();
}
}
// 處理連接
private void processConnection(Socket clientSocket) {
System.out.printf("[%s : %d] 客戶端建立連接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下來處理請(qǐng)求和響應(yīng)
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 把 inputStream 中的數(shù)據(jù)讀出來,寫入到 outputStream 中
// 循環(huán)地處理每個(gè)請(qǐng)求,分別返回響應(yīng)
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1、讀取請(qǐng)求
if (!scanner.hasNext()) {
System.out.printf("[%s : %d] 客戶端斷開連接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
String request = scanner.next(); // 此處用 Scanner 更方便,如果用 InputStream 的 read 也可以
// 2、根據(jù)請(qǐng)求,計(jì)算響應(yīng)
String response = process(request);
// 3、將這個(gè)響應(yīng)返回客戶端
// 方便起見,用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新,如果沒有這個(gè)刷新,可能客戶端就不能第一時(shí)間看到響應(yīng)結(jié)果
printWriter.flush();
System.out.printf("[%s : %d] req : %s, resp: %s!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 記得關(guān)閉!
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
server.start();
}
}
此時(shí)運(yùn)行,沒有問題:
改成多線程版本了之后,雖然前面的代碼已經(jīng)進(jìn)入到處理連接的邏輯了,但是并不影響第二次去調(diào)用 accept
服務(wù)器啟動(dòng)了!
[/127.0.0.1 : 7366] 客戶端建立連接!
[/127.0.0.1 : 7371] 客戶端建立連接!
[/127.0.0.1 : 7366] req : hello, resp: hello!
[/127.0.0.1 : 7371] req : java, resp: java!
當(dāng)前的這個(gè)問題,其實(shí)是電話打過去了,只是對(duì)方?jīng)]接聽,對(duì)方聽到響鈴了嘛?聽到了
嘗試建立連接的請(qǐng)求,已經(jīng)發(fā)過去,對(duì)方也知道了,只是對(duì)方不想理你而已
當(dāng)客戶端 new Socket 成功的時(shí)候,其實(shí)在操作系統(tǒng)內(nèi)核層面,已經(jīng)建立好連接了 ( TCP 三次握手),但是應(yīng)用程序 ,沒有接通這個(gè)連接
1.4、服務(wù)器 — 線程池
還是在剛剛的代碼中,改動(dòng) start()
的即可:
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpThreadPoolEchoServer {
// listen 英文原意:監(jiān)聽。但是 Java socket 中體現(xiàn)出 "監(jiān)聽" 的含義,
// 這樣叫是因?yàn)?,操作系統(tǒng)原生的 API 中,有一個(gè)操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadPoolEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務(wù)器啟動(dòng)了!");
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
// 1、建立連接
// 由于 TCP 是有連接的,不能一上來就讀數(shù)據(jù),需要先建立連接 (接電話)
// accept 就是在接電話,接電話的前提是,有人給你打,如果當(dāng)前客戶端嘗試建立連接,此處的 accept 就會(huì)阻塞
// accept 返回了一個(gè) socket 對(duì)象,稱為 clientSocket,后續(xù)和客戶端之間的溝通,都是都過 clientSocket 來完成的
Socket clientSocket = serverSocket.accept();
// [改進(jìn)方法] 在這里,每次 accept 成功,都創(chuàng)建一個(gè)新的線程,由新線程負(fù)責(zé)執(zhí)行這個(gè) processConnection 方法,串行變并發(fā)
// 通過線程池來實(shí)現(xiàn)
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
// 處理連接
private void processConnection(Socket clientSocket) {
System.out.printf("[%s : %d] 客戶端建立連接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下來處理請(qǐng)求和響應(yīng)
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 把 inputStream 中的數(shù)據(jù)讀出來,寫入到 outputStream 中
// 循環(huán)地處理每個(gè)請(qǐng)求,分別返回響應(yīng)
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1、讀取請(qǐng)求
if (!scanner.hasNext()) {
System.out.printf("[%s : %d] 客戶端斷開連接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
String request = scanner.next(); // 此處用 Scanner 更方便,如果用 InputStream 的 read 也可以
// 2、根據(jù)請(qǐng)求,計(jì)算響應(yīng)
String response = process(request);
// 3、將這個(gè)響應(yīng)返回客戶端
// 方便起見,用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新,如果沒有這個(gè)刷新,可能客戶端就不能第一時(shí)間看到響應(yīng)結(jié)果
printWriter.flush();
System.out.printf("[%s : %d] req : %s, resp: %s!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 記得關(guān)閉!
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9090);
server.start();
}
}
2、TCP 翻譯
繼承
TcpThreadPoolEchoServer
,將 process 改成 public
package network;
import java.io.IOException;
import java.util.HashMap;
public class TcpDictServer extends TcpThreadPoolEchoServer {
private HashMap<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("cat", "貓");
dict.put("dog", "狗");
dict.put("pig", "豬");
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "該詞無法被翻譯!");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}
根據(jù)請(qǐng)求計(jì)算響應(yīng),是一個(gè)服務(wù)器程序最最復(fù)雜的過程
問題:
一個(gè) TCP 服務(wù)器,能否讓一個(gè) UDP 客戶端連上?文章來源:http://www.zghlxwxcb.cn/news/detail-409486.html
TCP 和 UDP 他們無論是 API 代碼,還是協(xié)議底層 的工作過程,都是差異巨大的 (生殖隔離)。不是單純的 “把流轉(zhuǎn)成數(shù)據(jù)報(bào)” 就可以的,一次通信,需要用到五元組,協(xié)議類型不匹配,通信是無法完成的!文章來源地址http://www.zghlxwxcb.cn/news/detail-409486.html
到了這里,關(guān)于【Java 網(wǎng)絡(luò)編程】網(wǎng)絡(luò)通信原理、TCP、UDP 回顯服務(wù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!