在分布式架構(gòu)中格郁,有一個(gè)很重要的環(huán)節(jié)屉栓,就是分布式網(wǎng)絡(luò)中的計(jì)算機(jī)節(jié)點(diǎn)彼此之間需要通信。
HTTP 協(xié)議通信原理
說(shuō)到通信趟佃,大家一定聽(tīng)過(guò) tcp 和 udp 這兩種通信協(xié)議扇谣,以及建立連接的握手過(guò)程。而 http 協(xié)議的通信是基于 tcp/ip 協(xié)議之上的一個(gè)應(yīng)用層協(xié)議闲昭,應(yīng)用層協(xié)議除了 http 還有哪些呢(FTP罐寨、DNS、SMTP序矩、Telnet 等)拙泽。
涉及到網(wǎng)絡(luò)協(xié)議,我們一定需要知道 OSI 七層網(wǎng)絡(luò)模型和 TCP/IP 四層概念模型辈末,OSI 七層網(wǎng)絡(luò)模型包含(應(yīng)用層委可、表示層、會(huì)話層租幕、傳輸層舷手、網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層劲绪、物理層)男窟、TCP/IP 四層概念模型包含(應(yīng)用層、傳輸層贾富、網(wǎng)絡(luò)層歉眷、數(shù)據(jù)鏈路層)。
請(qǐng)求發(fā)起過(guò)程祷安,在 tcp/ip 四層網(wǎng)絡(luò)模型中所做的事情
當(dāng)應(yīng)用程序用 TCP 傳送數(shù)據(jù)時(shí)姥芥,數(shù)據(jù)被送入?yún)f(xié)議棧中,然后逐個(gè)通過(guò)每一層直到被當(dāng)作一串比特流送入網(wǎng)絡(luò)汇鞭。其中每一層對(duì)收到的數(shù)據(jù)都要增加一些首部信息(有時(shí)還要增加尾部信息)
客戶端如何找到目標(biāo)服務(wù)
在客戶端發(fā)起請(qǐng)求的時(shí)候凉唐,我們會(huì)在數(shù)據(jù)鏈路層去組裝目標(biāo)機(jī)器的 MAC 地址,目標(biāo)機(jī)器的mac 地址怎么得到呢霍骄? 這里就涉及到一個(gè) ARP 協(xié)議台囱,這個(gè)協(xié)議簡(jiǎn)單來(lái)說(shuō)就是已知目標(biāo)機(jī)器的 ip,需要獲得目標(biāo)機(jī)器的 mac 地址读整。(發(fā)送一個(gè)廣播消息簿训,這個(gè) ip 是誰(shuí)的,請(qǐng)來(lái)認(rèn)領(lǐng)。認(rèn)領(lǐng) ip 的機(jī)器會(huì)發(fā)送一個(gè) mac 地址的響應(yīng))
有了這個(gè)目標(biāo) MAC 地址强品,數(shù)據(jù)包在鏈路上廣播膘侮,MAC 的網(wǎng)卡才能發(fā)現(xiàn),這個(gè)包是給它的的榛。MAC 的網(wǎng)卡把包收進(jìn)來(lái)琼了,然后打開(kāi) IP 包,發(fā)現(xiàn) IP 地址也是自己的夫晌,再打開(kāi) TCP 包雕薪,發(fā)現(xiàn)端口是自己,也就是 80 端口晓淀,而這個(gè)時(shí)候這臺(tái)機(jī)器上有一個(gè) nginx 是監(jiān)聽(tīng) 80 端口所袁。于是將請(qǐng)求提交給 nginx,nginx 返回一個(gè)網(wǎng)頁(yè)凶掰。然后將網(wǎng)頁(yè)需要發(fā)回請(qǐng)求的機(jī)器燥爷。然后層層封裝,最后到 MAC 層懦窘。因?yàn)閬?lái)的時(shí)候有源 MAC 地址局劲,返回的時(shí)候,源 MAC 就變成了目標(biāo) MAC奶赠,再返給請(qǐng)求的機(jī)器。
為了避免每次都用 ARP 請(qǐng)求药有,機(jī)器本地也會(huì)進(jìn)行 ARP 緩存毅戈。當(dāng)然機(jī)器會(huì)不斷地上線下線,IP 也可能會(huì)變愤惰,所以 ARP 的 MAC 地址緩存過(guò)一段時(shí)間就會(huì)過(guò)期苇经。
接收端收到數(shù)據(jù)包以后的處理過(guò)程
當(dāng)目的主機(jī)收到一個(gè)以太網(wǎng)數(shù)據(jù)幀時(shí),數(shù)據(jù)就開(kāi)始從協(xié)議棧中由底向上升宦言,同時(shí)去掉各層協(xié)議加上的報(bào)文首部扇单。每層協(xié)議都要去檢查報(bào)文首部中的協(xié)議標(biāo)識(shí),以確定接收數(shù)據(jù)的上層協(xié)議奠旺。
為什么有了 MAC 層還要走 IP 層呢蜘澜?
之前我們提到,mac 地址是唯一的响疚,那理論上鄙信,在任何兩個(gè)設(shè)備之間,我應(yīng)該都可以通過(guò)
mac 地址發(fā)送數(shù)據(jù)忿晕,為什么還需要 ip 地址装诡?
mac 地址就好像個(gè)人的身份證號(hào),人的身份證號(hào)和人戶口所在的城市,出生的日期有關(guān)鸦采,但是和人所在的位置沒(méi)有關(guān)系宾巍,人是會(huì)移動(dòng)的,知道一個(gè)人的身份證號(hào)渔伯,并不能找到它這個(gè)人顶霞,mac 地址類(lèi)似,它是和設(shè)備的生產(chǎn)者咱旱,批次确丢,日期之類(lèi)的關(guān)聯(lián)起來(lái),知道一個(gè)設(shè)備的mac吐限,并不能在網(wǎng)絡(luò)中將數(shù)據(jù)發(fā)送給它鲜侥,除非它和發(fā)送方的在同一個(gè)網(wǎng)絡(luò)內(nèi)。
所以要實(shí)現(xiàn)機(jī)器之間的通信诸典,我們還需要有 ip 地址的概念描函,ip 地址表達(dá)的是當(dāng)前機(jī)器在網(wǎng)絡(luò)中的位置,類(lèi)似于城市名+道路號(hào)+門(mén)牌號(hào)的概念狐粱。通過(guò) ip 層的尋址舀寓,我們能知道按何種路徑在全世界任意兩臺(tái) Internet 上的的機(jī)器間傳輸數(shù)據(jù)。
TCP/IP 的分層管理
TCP/IP 協(xié)議按照層次分為 4 層:應(yīng)用層肌蜻、傳輸層互墓、網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層蒋搜。對(duì)于分層這個(gè)概念篡撵,大家一定不陌生,比如我們的分布式架構(gòu)體系中會(huì)分為業(yè)務(wù)層豆挽、服務(wù)層育谬、基礎(chǔ)支撐層。比如docker帮哈,也是基于分層來(lái)實(shí)現(xiàn)膛檀。所以我們會(huì)發(fā)現(xiàn),復(fù)雜的程序都需要分層娘侍,這個(gè)是軟件設(shè)計(jì)的要求咖刃,每一層專(zhuān)注于當(dāng)前領(lǐng)域的事情。如果某些地方需要修改憾筏,我們只需要把變動(dòng)的層替換掉就行僵缺,一方面改動(dòng)影響較少,另一方面整個(gè)架構(gòu)的靈活性也更高踩叭。 最后磕潮,在分層之后翠胰,整個(gè)架構(gòu)的設(shè)計(jì)也變得相對(duì)簡(jiǎn)單了。
分層負(fù)載
了解了分層的概念以后自脯,我們?cè)偃ダ斫馑^的二層負(fù)載之景、三層負(fù)載、四層負(fù)載膏潮、七層負(fù)載就容易多了锻狗。
一次 http 請(qǐng)求過(guò)來(lái),一定會(huì)從應(yīng)用層到傳輸層焕参,完成整個(gè)交互轻纪。只要是在網(wǎng)絡(luò)上跑的數(shù)據(jù)包,都是完整的叠纷】讨悖可以有下層沒(méi)上層,絕對(duì)不可能有上層沒(méi)下層涩嚣。
二層負(fù)載均衡
二層負(fù)載是針對(duì) MAC崇众,負(fù)載均衡服務(wù)器對(duì)外依然提供一個(gè) VIP(虛 IP),集群中不同的機(jī)器采用相同 IP 地址航厚,但是機(jī)器的 MAC 地址不一樣顷歌。當(dāng)負(fù)載均衡服務(wù)器接受到請(qǐng)求之后,通過(guò)改寫(xiě)報(bào)文的目標(biāo) MAC 地址的方式將請(qǐng)求轉(zhuǎn)發(fā)到目標(biāo)機(jī)器實(shí)現(xiàn)負(fù)載均衡
二層負(fù)載均衡會(huì)通過(guò)一個(gè)虛擬 MAC 地址接收請(qǐng)求幔睬,然后再分配到真實(shí)的 MAC 地址
三層負(fù)載均衡
三層負(fù)載是針對(duì) IP眯漩,和二層負(fù)載均衡類(lèi)似,負(fù)載均衡服務(wù)器對(duì)外依然提供一個(gè) VIP(虛 IP)麻顶,但是集群中不同的機(jī)器采用不同的 IP 地址坤塞。當(dāng)負(fù)載均衡服務(wù)器接受到請(qǐng)求之后,根據(jù)不同的負(fù)載均衡算法澈蚌,通過(guò) IP 將請(qǐng)求轉(zhuǎn)發(fā)至不同的真實(shí)服務(wù)器
三層負(fù)載均衡會(huì)通過(guò)一個(gè)虛擬 IP 地址接收請(qǐng)求,然后再分配到真實(shí)的 IP 地址
四層負(fù)載均衡
四層負(fù)載均衡工作在 OSI 模型的傳輸層灼狰,由于在傳輸層宛瞄,只有 TCP/UDP 協(xié)議,這兩種協(xié)議中除了包含源 IP交胚、目標(biāo) IP 以外份汗,還包含源端口號(hào)及目的端口號(hào)。四層負(fù)載均衡服務(wù)器在接受到客戶端請(qǐng)求后蝴簇,以后通過(guò)修改數(shù)據(jù)包的地址信息(IP+端口號(hào))將流量轉(zhuǎn)發(fā)到應(yīng)用服務(wù)器杯活。
四層通過(guò)虛擬 IP + 端口接收請(qǐng)求,然后再分配到真實(shí)的服務(wù)器
七層負(fù)載均衡
七層負(fù)載均衡工作在 OSI 模型的應(yīng)用層熬词,應(yīng)用層協(xié)議較多旁钧,常用 http吸重、radius、dns 等歪今。七層負(fù)載就可以基于這些協(xié)議來(lái)負(fù)載嚎幸。這些應(yīng)用層協(xié)議中會(huì)包含很多有意義的內(nèi)容。比如同一個(gè)Web 服務(wù)器的負(fù)載均衡寄猩,除了根據(jù) IP 加端口進(jìn)行負(fù)載外嫉晶,還可根據(jù)七層的 URL、瀏覽器類(lèi)別來(lái)決定是否要進(jìn)行負(fù)載均衡
七層通過(guò)虛擬的 URL 或主機(jī)名接收請(qǐng)求田篇,然后再分配到真實(shí)的服務(wù)器替废。
TCP/IP 協(xié)議的深入分析
TCP 握手協(xié)議
以 TCP 消息的可靠性首先來(lái)自于有效的連接建立,所以在數(shù)據(jù)進(jìn)行傳輸前泊柬,需要通過(guò)三次握手建立一個(gè)連接椎镣,所謂的三次握手,就是在建立 TCP 鏈接時(shí)彬呻,需要客戶端和服務(wù)端總共發(fā)送 3 個(gè)包來(lái)確認(rèn)連接的建立衣陶,在 socket 編程中,這個(gè)過(guò)程由客戶端執(zhí)行 connect 來(lái)觸發(fā)
第 一 次 握 手(SYN=1, seq=x)客 戶 端 發(fā) 送 一 個(gè)TCP 的 SYN 標(biāo)志位置 1 的包闸氮,指明客戶端打算連接的服務(wù)器的端口剪况,以及初始序號(hào) X,保存在 包 頭 的 序 列 號(hào)(Sequence Number)字段里。發(fā)送完畢后蒲跨,客戶端 進(jìn) 入SYN_SEND 狀態(tài)译断。
第 二 次 握 手(SYN=1, ACK=1, seq=y, ACKnum=x+1):服務(wù)器發(fā)回確認(rèn)包(ACK) 應(yīng) 答 。 即SYN 標(biāo)志位和ACK 標(biāo) 志 位 均 為1或悲。服務(wù)器端選擇自己 ISN 序列號(hào)孙咪,放到 Seq 域里,同時(shí)將 確 認(rèn) 序 號(hào)(Acknowledgement Number)設(shè)置為客戶的 ISN 加 1巡语,即 X+1翎蹈。發(fā)送完畢后,服務(wù)器 端 進(jìn) 入SYN_RCVD 狀態(tài)男公。
第 三 次 握 手(ACK=1 荤堪,ACKnum=y+1)客戶端再次發(fā)送確認(rèn)包(ACK),SYN 標(biāo)志位為 0枢赔,ACK 標(biāo)
志位為 1澄阳,并且把服務(wù)器發(fā)來(lái) ACK 的序號(hào)字段+1,放在確定字段中發(fā)送給對(duì)方踏拜,并且在數(shù)據(jù)段放寫(xiě) ISN 發(fā)完畢后 碎赢, 客 戶 端 進(jìn) 入ESTABLISHED 狀態(tài),當(dāng)服務(wù)器端接收到這個(gè)包時(shí)速梗,也進(jìn) 入ESTABLISHED 狀態(tài)肮塞,TCP 握手結(jié)束襟齿。
那 TCP 在三次握手的時(shí)候,IP 層和 MAC 層在做什么呢峦嗤?當(dāng)然是 TCP 發(fā)送每一個(gè)消息蕊唐,都會(huì)帶著 IP 層和 MAC 層了。因?yàn)樗干瑁琓CP 每發(fā)送一個(gè)消息替梨,IP 層和 MAC 層的所有機(jī)制都要運(yùn)行一遍。而你只看到 TCP 三次握手了装黑,其實(shí)副瀑,IP 層和 MAC 層為此也忙活好久了。
SYN 攻擊
在三次握手過(guò)程中恋谭,Server 發(fā)送 SYN-ACK 之后糠睡,收到 Client 的 ACK 之前的 TCP 連接稱(chēng)為半連接(half-open connect),此時(shí) Server 處于 SYN_RCVD 狀態(tài)疚颊,當(dāng)收到 ACK 后狈孔,Server轉(zhuǎn)入 ESTABLISHED 狀態(tài)。SYN 攻擊就是 Client 在短時(shí)間內(nèi)偽造大量不存在的 IP 地址材义,并向Server 不斷地發(fā)送 SYN 包均抽,Server 回復(fù)確認(rèn)包,并等待 Client 的確認(rèn)其掂,由于源地址是不存在的油挥,因此,Server 需要不斷重發(fā)直至超時(shí)款熬,這些偽造的 SYN 包將產(chǎn)時(shí)間占用未連接隊(duì)列深寥,導(dǎo)致正常的 SYN 請(qǐng)求因?yàn)殛?duì)列滿而被丟棄,從而引起網(wǎng)絡(luò)堵塞甚至系統(tǒng)癱瘓贤牛。SYN 攻擊時(shí)一種典型的 DDOS 攻擊惋鹅,檢測(cè) SYN 攻擊的方式非常簡(jiǎn)單,即當(dāng) Server 上有大量半連接狀態(tài)且源 IP 地址是隨機(jī)的殉簸,則可以斷定遭到 SYN 攻擊了
TCP 四次揮手協(xié)議
四次揮手表示 TCP 斷開(kāi)連接的時(shí)候,需要客戶端和服務(wù)端總共發(fā)送 4 個(gè)包以確認(rèn)連接的斷開(kāi)闰集;
客戶端或服務(wù)器均可主動(dòng)發(fā)起揮手動(dòng)作(因?yàn)?TCP 是一個(gè)全雙工協(xié)議),在 socket 編程中喂链,任何一方執(zhí)行 close() 操作即可產(chǎn)生揮手操作。
單工:數(shù)據(jù)傳輸只支持?jǐn)?shù)據(jù)在一個(gè)方向上傳輸
半雙工:數(shù)據(jù)傳輸允許數(shù)據(jù)在兩個(gè)方向上傳輸妥泉,但是在某一時(shí)刻椭微,只允許在一個(gè)方向上傳輸,實(shí)際上有點(diǎn)像切換方向的單工通信
全雙工:數(shù)據(jù)通信允許數(shù)據(jù)同時(shí)在兩個(gè)方向上傳輸盲链,因此全雙工是兩個(gè)單工通信方式的結(jié)合蝇率,它要求發(fā)送設(shè)備和接收設(shè)備都有獨(dú)立的接收和發(fā)送能力
第一次揮手(FIN=1迟杂,seq=x)
假設(shè)客戶端想要關(guān)閉連接,客戶端發(fā)送一個(gè) FIN 標(biāo)志位置為 1 的包本慕,表示自己已經(jīng)沒(méi)有數(shù)據(jù)可以發(fā)送了排拷,但是仍然可以接受數(shù)據(jù)。發(fā)送完畢后锅尘,客戶端進(jìn)入 FIN_WAIT_1 狀態(tài)监氢。
第二次揮手(ACK=1,ACKnum=x+1)
服務(wù)器端確認(rèn)客戶端的 FIN 包藤违,發(fā)送一個(gè)確認(rèn)包浪腐,表明自己接受到了客戶端關(guān)閉連接的請(qǐng)求,但沒(méi)有準(zhǔn)備好關(guān)閉連接顿乒。發(fā)送完畢后议街,服務(wù)器端進(jìn)入 CLOSE_WAIT 狀態(tài),客戶端接收到這個(gè)確認(rèn)包之后璧榄,進(jìn)入 FIN_WAIT_2 狀態(tài)特漩,等待服務(wù)器端關(guān)閉連接。
第三次揮手(FIN=1骨杂,seq=w)
服務(wù)器端準(zhǔn)備好關(guān)閉連接時(shí)涂身,向客戶端發(fā)送結(jié)束連接請(qǐng)求,F(xiàn)IN 置為 1腊脱。發(fā)送完畢后访得,服務(wù)器端進(jìn)入 LAST_ACK 狀態(tài),等待來(lái)自客戶端的最后一個(gè) ACK陕凹。
第四次揮手(ACK=1悍抑,ACKnum=w+1)
客戶端接收到來(lái)自服務(wù)器端的關(guān)閉請(qǐng)求,發(fā)送一個(gè)確認(rèn)包杜耙,并進(jìn)入 TIME_WAIT 狀態(tài)搜骡,等待可能出現(xiàn)的要求重傳的 ACK 包。服務(wù)器端接收到這個(gè)確認(rèn)包之后佑女,關(guān)閉連接记靡,進(jìn)入 CLOSED 狀態(tài)。
客戶端等待了某個(gè)固定時(shí)間(兩個(gè)最大段生命周期团驱,2MSL摸吠,2 Maximum Segment Lifetime)之后,沒(méi)有收到服務(wù)器端的 ACK嚎花,認(rèn)為服務(wù)器端已經(jīng)正常關(guān)閉連接寸痢,于是自己也關(guān)閉連接,進(jìn)入 CLOSED 狀態(tài)紊选。
假設(shè) Client 端發(fā)起中斷連接請(qǐng)求啼止,也就是發(fā)送 FIN 報(bào)文道逗。Server 端接到 FIN 報(bào)文后,意思是說(shuō)"我 Client 端沒(méi)有數(shù)據(jù)要發(fā)給你了"献烦,但是如果你還有數(shù)據(jù)沒(méi)有發(fā)送完成滓窍,則不必急著關(guān)閉Socket,可以繼續(xù)發(fā)送數(shù)據(jù)巩那。所以你先發(fā)送 ACK吏夯,"告訴 Client 端,你的請(qǐng)求我收到了拢操,但是我還沒(méi)準(zhǔn)備好锦亦,請(qǐng)繼續(xù)你等我的消息"。這個(gè)時(shí)候 Client 端就進(jìn)入 FIN_WAIT 狀態(tài)令境,繼續(xù)等待Server 端的 FIN 報(bào)文杠园。當(dāng) Server 端確定數(shù)據(jù)已發(fā)送完成,則向 Client 端發(fā)送 FIN 報(bào)文舔庶,"告訴 Client 端抛蚁,好了,我這邊數(shù)據(jù)發(fā)完了惕橙,準(zhǔn)備好關(guān)閉連接了"瞧甩。Client 端收到 FIN 報(bào)文后,"就知道可以關(guān)閉連接了弥鹦,但是他還是不相信網(wǎng)絡(luò)肚逸,怕 Server 端不知道要關(guān)閉,所以發(fā)送 ACK 后進(jìn)入 TIME_WAIT 狀態(tài)彬坏,如果 Server 端沒(méi)有收到 ACK 則可以重傳朦促。“栓始,Server 端收到 ACK 后务冕,"就知道可以斷開(kāi)連接了"。Client 端等待了 2MSL 后依然沒(méi)有收到回復(fù)幻赚,則證明 Server 端已正常關(guān)閉禀忆,那好,我 Client 端也可以關(guān)閉連接了落恼。Ok箩退,TCP 連接就這樣關(guān)閉了!
問(wèn)題
【問(wèn)題 1】為什么連接的時(shí)候是三次握手佳谦,關(guān)閉的時(shí)候卻是四次握手戴涝?
答:三次握手是因?yàn)橐驗(yàn)楫?dāng) Server 端收到 Client 端的 SYN 連接請(qǐng)求報(bào)文后,可以直接發(fā)送SYN+ACK 報(bào)文。其中 ACK 報(bào)文是用來(lái)應(yīng)答的喊括,SYN 報(bào)文是用來(lái)同步的。但是關(guān)閉連接時(shí)矢棚,當(dāng) Server 端收到 FIN 報(bào)文時(shí)郑什,很可能并不會(huì)立即關(guān)閉 SOCKET(因?yàn)榭赡苓€有消息沒(méi)處理完),所以只能先回復(fù)一個(gè) ACK 報(bào)文蒲肋,告訴 Client 端蘑拯,"你發(fā)的 FIN 報(bào)文我收到了"。只有等到我 Server 端所有的報(bào)文都發(fā)送完了兜粘,我才能發(fā)送 FIN 報(bào)文申窘,因此不能一起發(fā)送。故需要四步握手孔轴。
【問(wèn)題 2】為什么 TIME_WAIT 狀態(tài)需要經(jīng)過(guò) 2MSL(最大報(bào)文段生存時(shí)間)才能返回到 CLOSE狀態(tài)剃法?
答:雖然按道理,四個(gè)報(bào)文都發(fā)送完畢路鹰,我們可以直接進(jìn)入 CLOSE 狀態(tài)了贷洲,但是我們必須假象網(wǎng)絡(luò)是不可靠的,有可以最后一個(gè) ACK 丟失晋柱。所以 TIME_WAIT 狀態(tài)就是用來(lái)重發(fā)可能丟失的 ACK 報(bào)文优构。
使用協(xié)議進(jìn)行通信
tcp 連接建立以后,就可以基于這個(gè)連接通道來(lái)發(fā)送和接受消息了雁竞,TCP钦椭、UDP 都是在基于Socket 概念上為某類(lèi)應(yīng)用場(chǎng)景而擴(kuò)展出的傳輸協(xié)議,那么什么是 socket 呢碑诉?socket 是一種抽象層彪腔,應(yīng)用程序通過(guò)它來(lái)發(fā)送和接收數(shù)據(jù),就像應(yīng)用程序打開(kāi)一個(gè)文件句柄联贩,把數(shù)據(jù)讀寫(xiě)到磁盤(pán)上一樣漫仆。使用 socket 可以把應(yīng)用程序添加到網(wǎng)絡(luò)中,并與處于同一個(gè)網(wǎng)絡(luò)中的其他應(yīng)用程序進(jìn)行通信泪幌。不同類(lèi)型的 Socket 與不同類(lèi)型的底層協(xié)議簇有關(guān)聯(lián)盲厌。主要的 socket 類(lèi)型為流套接字(stream socket)和數(shù)據(jù)報(bào)文套接字(datagram socket)。 stream socket 把 TCP作為端對(duì)端協(xié)議(底層使用 IP 協(xié)議)祸泪,提供一個(gè)可信賴(lài)的字節(jié)流服務(wù)吗浩。數(shù)據(jù)報(bào)文套接字(datagram socket)使用 UDP 協(xié)議(底層同樣使用 IP 協(xié)議)提供了一種“盡力而為”的數(shù)據(jù)報(bào)文服務(wù)。
接下來(lái)没隘,我們使用 Java 提供的 API 來(lái)展示 TCP 協(xié)議的客戶端和服務(wù)端通信的案例和 UDP協(xié)議的客戶端和服務(wù)端通信的案例懂扼,然后更進(jìn)一步了解底層的原理
基于 TCP 協(xié)議實(shí)現(xiàn)通信
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的從客戶端發(fā)送一個(gè)消息到服務(wù)端的功能
服務(wù)端代碼如下:
public static void main(String[] args)
throws IOException {
ServerSocket serverSocket = null;
BufferedReader in = null;
try {
/*TCP 的服務(wù)端要先監(jiān)聽(tīng)一個(gè)端口,一般是先調(diào)用bind 函數(shù)棚愤,
給這個(gè) Socket 賦予一個(gè) IP 地址和端 口触创。為什么需要端口呢校读?要知道徒恋,
你寫(xiě)的是一個(gè)應(yīng)用程序石蔗,當(dāng)一個(gè)網(wǎng)絡(luò)包來(lái)的時(shí)候每聪,內(nèi)核要通過(guò) TCP 頭里 面的這個(gè)端口忍法,
來(lái)找到你這個(gè)應(yīng)用程序面粮,把包給你灾挨。
為什么要 IP 地址呢邑退?
有時(shí)候,一臺(tái)機(jī)器會(huì)有多個(gè)網(wǎng) 卡劳澄,也就會(huì)有多個(gè) IP 地址地技,
你可以選擇監(jiān)聽(tīng)所有的網(wǎng)卡,也可以選擇監(jiān)聽(tīng)一個(gè)網(wǎng)卡秒拔,
這樣莫矗,只有發(fā)給這個(gè)網(wǎng)卡的包,才會(huì)給你砂缩。
serverSocket = new ServerSocket(8080);
//阻塞等待客戶端連接
接下來(lái)趣苏,服務(wù)端調(diào)用 accept 函數(shù),拿出一個(gè)已經(jīng)完成的連接進(jìn)行處理梯轻。
如果還沒(méi)有完成食磕,就要等著。
java.net.Socket socket = serverSocket.accept();
連接建立成功之后喳挑,雙方開(kāi)始通過(guò) read 和 write函數(shù)來(lái)讀寫(xiě)數(shù)據(jù)彬伦,
就像往一個(gè)文件流里面寫(xiě)東西一樣*/
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(in.readLine());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (serverSocket != null) {
serverSocket.close();
}
}
}
客戶端代碼如下:
public static void main(String[] args) {
Socket socket = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", 8080);
out = new PrintWriter(socket.getOutputStream(), true);
out.println("Hello, ");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
理解 TCP 的通信原理及 IO 阻塞
通過(guò)上面這個(gè)簡(jiǎn)單的案例,基本清楚了在 Java 應(yīng)用程序中如何使用 socket 套接字來(lái)建立一個(gè)基于 tcp 協(xié)議的通信流程伊诵。接下來(lái)单绑,我們?cè)趤?lái)了解一下 tcp 的底層通信過(guò)程是什么樣的
了解 TCP 協(xié)議的通信過(guò)程
首先,對(duì)于 TCP 通信來(lái)說(shuō)曹宴,每個(gè) TCP Socket 的內(nèi)核中都有一個(gè)發(fā)送緩沖區(qū)和一個(gè)接收緩沖區(qū)搂橙,TCP 的全雙工的工作模式及 TCP 的滑動(dòng)窗口就是依賴(lài)于這兩個(gè)獨(dú)立的 Buffer 和該 Buffer的填充狀態(tài)。
接收緩沖區(qū)把數(shù)據(jù)緩存到內(nèi)核笛坦,若應(yīng)用進(jìn)程一直沒(méi)有調(diào)用 Socket 的 read 方法進(jìn)行讀取区转,那么該數(shù)據(jù)會(huì)一直被緩存在接收緩沖區(qū)內(nèi)。不管進(jìn)程是否讀取 Socket版扩,對(duì)端發(fā)來(lái)的數(shù)據(jù)都會(huì)經(jīng)過(guò)內(nèi)核接收并緩存到 Socket 的內(nèi)核接收緩沖區(qū)废离。
read 所要做的工作,就是把內(nèi)核接收緩沖區(qū)中的數(shù)據(jù)復(fù)制到應(yīng)用層用戶的 Buffer 里礁芦。
進(jìn)程調(diào)用 Socket 的 send 發(fā)送數(shù)據(jù)的時(shí)候蜻韭,一般情況下是將數(shù)據(jù)從應(yīng)用層用戶的 Buffer 里復(fù)制到 Socket 的內(nèi)核發(fā)送緩沖區(qū)悼尾,然后 send 就會(huì)在上層返回。換句話說(shuō)肖方,send 返回時(shí)闺魏,數(shù)據(jù)不一定會(huì)被發(fā)送到對(duì)端。
前面我們提到俯画,Socket 的接收緩沖區(qū)被 TCP 用來(lái)緩存網(wǎng)絡(luò)上收到的數(shù)據(jù)舷胜,一直保存到應(yīng)用進(jìn)程讀走為止。如果應(yīng)用進(jìn)程一直沒(méi)有讀取活翩,那么 Buffer 滿了以后,出現(xiàn)的情況是:通知對(duì)端TCP 協(xié)議中的窗口關(guān)閉翻伺,保證 TCP 接收緩沖區(qū)不會(huì)移除材泄,保證了 TCP 是可靠傳輸?shù)摹H绻麑?duì)方無(wú)視窗口大小發(fā)出了超過(guò)窗口大小的數(shù)據(jù)吨岭,那么接收方會(huì)把這些數(shù)據(jù)丟棄拉宗。
滑動(dòng)窗口協(xié)議
這個(gè)過(guò)程中涉及到了 TCP 的滑動(dòng)窗口協(xié)議,滑動(dòng)窗口(Sliding window)是一種流量控制技術(shù)辣辫。早期的網(wǎng)絡(luò)通信中旦事,通信雙方不會(huì)考慮網(wǎng)絡(luò)的擁擠情況直接發(fā)送數(shù)據(jù)。由于大家不知道網(wǎng)絡(luò)擁塞狀況急灭,同時(shí)發(fā)送數(shù)據(jù)姐浮,導(dǎo)致中間節(jié)點(diǎn)阻塞掉包,誰(shuí)也發(fā)不了數(shù)據(jù)葬馋,所以就有了滑動(dòng)窗口機(jī)制來(lái)解決此問(wèn)題卖鲤;發(fā)送和接受方都會(huì)維護(hù)一個(gè)數(shù)據(jù)幀的序列,這個(gè)序列被稱(chēng)作窗口
- 發(fā)送窗口
就是發(fā)送端允許連續(xù)發(fā)送的幀的序號(hào)表畴嘶。
發(fā)送端可以不等待應(yīng)答而連續(xù)發(fā)送的最大幀數(shù)稱(chēng)為發(fā)送窗口的尺寸蛋逾。 - 接收窗口
接收方允許接收的幀的序號(hào)表,凡落在 接收窗口內(nèi)的幀窗悯,接收方都必須處理区匣,落在接收窗口外的幀被丟棄。
接收方每次允許接收的幀數(shù)稱(chēng)為接收窗口的尺寸蒋院。
在線滑動(dòng)窗口演示功能
https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html
到底什么是阻塞
了解了基本通信原理以后亏钩,我們?cè)賮?lái)思考一個(gè)問(wèn)題,在前面的代碼演示中欺旧,我們通過(guò)socket.accept 去接收一個(gè)客戶端請(qǐng)求铸屉,accept 是一個(gè)阻塞的方法,意味著 TCP 服務(wù)器一次只能處理一個(gè)客戶端請(qǐng)求切端,當(dāng)一個(gè)客戶端向一個(gè)已經(jīng)被其他客戶端占用的服務(wù)器發(fā)送連接請(qǐng)求時(shí)彻坛,雖然在連接建立后可以向服務(wù)端發(fā)送數(shù)據(jù),但是在服務(wù)端處理完之前的請(qǐng)求之前,卻不會(huì)對(duì)新的客戶端做出響應(yīng)昌屉,這種類(lèi)型的服務(wù)器稱(chēng)為“迭代服務(wù)器”钙蒙。迭代服務(wù)器是按照順序處理客戶端請(qǐng)求,也就是服務(wù)端必須要處理完前一個(gè)請(qǐng)求才能對(duì)下一個(gè)客戶端的請(qǐng)求進(jìn)行響應(yīng)间驮。
但是在實(shí)際應(yīng)用中躬厌,我們不能接收這樣的處理方式。所以我們需要一種方法可以獨(dú)立處理每一個(gè)連接竞帽,并且他們之間不會(huì)相互干擾扛施。而 Java 提供的多線程技術(shù)剛好滿足這個(gè)需求,這個(gè)機(jī)制使得服務(wù)器能夠方便處理多個(gè)客戶端的請(qǐng)求屹篓。
一個(gè)客戶端對(duì)應(yīng)一個(gè)線程
為每個(gè)客戶端創(chuàng)建一個(gè)線程實(shí)際上會(huì)存在一些弊端疙渣,因?yàn)閯?chuàng)建一個(gè)線程需要占用 CPU 的資源和內(nèi)存資源。另外堆巧,隨著線程數(shù)增加妄荔,系統(tǒng)資源將會(huì)成為瓶頸最終達(dá)到一個(gè)不可控的狀態(tài),所以我們還可以通過(guò)線程池來(lái)實(shí)現(xiàn)多個(gè)客戶端請(qǐng)求的功能谍肤,因?yàn)榫€程池是可控的啦租。
非阻塞模型
上面這種模型雖然優(yōu)化了 IO 的處理方式,但是荒揣,不管是線程池還是單個(gè)線程篷角,線程本身的處理個(gè)數(shù)是有限制的,對(duì)于操作系統(tǒng)來(lái)說(shuō)系任,如果線程數(shù)太多會(huì)造成 CPU 上下文切換的開(kāi)銷(xiāo)内地。因此這種方式不能解決根本問(wèn)題。
阻塞 IO
當(dāng)客戶端的數(shù)據(jù)從網(wǎng)卡緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)之前赋除,服務(wù)端會(huì)一直阻塞阱缓。以socket接口為例,進(jìn)程空間中調(diào)用 recvfrom举农,進(jìn)程從調(diào)用 recvfrom 開(kāi)始到它返回的整段時(shí)間內(nèi)都是被阻塞的荆针,因此被成為阻塞 IO 模型
非阻塞 IO
思考一個(gè)問(wèn)題,如果我們希望這臺(tái)服務(wù)器能夠處理更多的連接颁糟,怎么去優(yōu)化呢航背?
我們第一時(shí)間想到的應(yīng)該是如何保證這個(gè)阻塞變成非阻塞吧。所以就引入了非阻塞 IO 模型棱貌,非阻塞 IO 模型的原理很簡(jiǎn)單玖媚,就是進(jìn)程空間調(diào)用 recvfrom,如果這個(gè)時(shí)候內(nèi)核緩沖區(qū)沒(méi)有數(shù)據(jù)的話婚脱,就直接返回一個(gè) EWOULDBLOCK 錯(cuò)誤今魔,然后應(yīng)用程序通過(guò)不斷輪詢(xún)來(lái)檢查這個(gè)狀態(tài)狀態(tài)勺像,看內(nèi)核是不是有數(shù)據(jù)過(guò)來(lái)。
I/O 復(fù)用模型
前面講的非阻塞仍然需要進(jìn)程不斷的輪詢(xún)重試错森。能不能實(shí)現(xiàn)當(dāng)數(shù)據(jù)可讀了以后給程序一個(gè)通知呢吟宦?所以這里引入了一個(gè) IO 多路復(fù)用模型,I/O 多路復(fù)用的本質(zhì)是通過(guò)一種機(jī)制(系統(tǒng)內(nèi)核緩沖 I/O 數(shù)據(jù))涩维,讓單個(gè)進(jìn)程可以監(jiān)視多個(gè)文件描述符殃姓,一旦某個(gè)描述符就緒(一般是讀就緒或?qū)懢途w),能夠通知程序進(jìn)行相應(yīng)的讀寫(xiě)操作
【什么是 fd:在 linux 中瓦阐,內(nèi)核把所有的外部設(shè)備都當(dāng)成是一個(gè)文件來(lái)操作蜗侈,對(duì)一個(gè)文件的讀寫(xiě)會(huì)調(diào)用內(nèi)核提供的系統(tǒng)命令,返回一個(gè) fd(文件描述符)睡蟋。而對(duì)于一個(gè) socket 的讀寫(xiě)也會(huì)有相應(yīng)的文件描述符踏幻,成為 socketfd】
常見(jiàn)的 IO 多路復(fù)用方式有【select、poll薄湿、epoll】,都是 Linux API 提供的 IO 復(fù)用方式偷卧,那么接下來(lái)重點(diǎn)講一下 select豺瘤、和 epoll 這兩個(gè)模型
- select:進(jìn)程可以通過(guò)把一個(gè)或者多個(gè) fd 傳遞給 select 系統(tǒng)調(diào)用,進(jìn)程會(huì)阻塞在 select 操作上听诸,這樣 select 可以幫我們檢測(cè)多個(gè) fd 是否處于就緒狀態(tài)坐求。
這個(gè)模式有二個(gè)缺點(diǎn)
- 由于他能夠同時(shí)監(jiān)聽(tīng)多個(gè)文件描述符,假如說(shuō)有 1000 個(gè)晌梨,這個(gè)時(shí)候如果其中一個(gè) fd 處于就緒狀態(tài)了桥嗤,那么當(dāng)前進(jìn)程需要線性輪詢(xún)所有的 fd,也就是監(jiān)聽(tīng)的 fd 越多仔蝌,性能開(kāi)銷(xiāo)越大泛领。
- 同時(shí),select 在單個(gè)進(jìn)程中能打開(kāi)的 fd 是有限制的敛惊,默認(rèn)是 1024渊鞋,對(duì)于那些需要支持單機(jī)上萬(wàn)的 TCP 連接來(lái)說(shuō)確實(shí)有點(diǎn)少
- epoll:linux 還提供了 epoll 的系統(tǒng)調(diào)用,epoll 是基于事件驅(qū)動(dòng)方式來(lái)代替順序掃描瞧挤,因此性能相對(duì)來(lái)說(shuō)更高锡宋,主要原理是,當(dāng)被監(jiān)聽(tīng)的 fd 中特恬,有 fd 就緒時(shí)执俩,會(huì)告知當(dāng)前進(jìn)程具體哪一個(gè) fd 就緒,那么當(dāng)前進(jìn)程只需要去從指定的 fd 上讀取數(shù)據(jù)即可
另外癌刽,epoll 所能支持的 fd 上線是操作系統(tǒng)的最大文件句柄役首,這個(gè)數(shù)字要遠(yuǎn)遠(yuǎn)大于 1024
【由于 epoll 能夠通過(guò)事件告知應(yīng)用進(jìn)程哪個(gè) fd 是可讀的尝丐,所以我們也稱(chēng)這種 IO 為異步非阻塞 IO,當(dāng)然它是偽異步的宋税,因?yàn)樗€需要去把數(shù)據(jù)從內(nèi)核同步復(fù)制到用戶空間中摊崭,真正的異步非阻塞,應(yīng)該是數(shù)據(jù)已經(jīng)完全準(zhǔn)備好了杰赛,我只需要從用戶空間讀就行】
多路復(fù)用的好處
I/O 多路復(fù)用可以通過(guò)把多個(gè) I/O 的阻塞復(fù)用到同一個(gè) select 的阻塞上呢簸,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請(qǐng)求。它的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷(xiāo)小乏屯,并且不需要?jiǎng)?chuàng)建新的進(jìn)程或者線程根时,降低了系統(tǒng)的資源開(kāi)銷(xiāo)
一臺(tái)機(jī)器理論能支持的連接數(shù)
再補(bǔ)充一點(diǎn)小知識(shí),理論上一臺(tái)機(jī)器能夠支撐多少個(gè)連接辰晕?
首先蛤迎,在確定最大連接數(shù)之前,大家先跟我來(lái)先了解一下系統(tǒng)如何標(biāo)識(shí)一個(gè) tcp 連接含友。系統(tǒng)用 一 個(gè) 四 元 組 來(lái) 唯 一 標(biāo) 識(shí) 一 個(gè) TCP 連接: (source_ip, source_port, destination_ip, destination_port)替裆。即(源 IP,源端口窘问,目的 IP辆童,目的端口)四個(gè)元素的組合。只要四個(gè)元素的組合中有一個(gè)元素不一樣惠赫,那就可以區(qū)別不同的連接把鉴,
比如:
你的 IP 地址是 11.1.2.3, 在 8080 端口監(jiān)聽(tīng)
那么當(dāng)一個(gè)來(lái)自 22.4.5.6 ,端口為 5555 的連接到達(dá)后儿咱,那么建立的這條連接的四元組為 : (11.1.2.3, 8080, 22.4.5.6, 5555)
這時(shí)庭砍,假設(shè)上面的那個(gè)客戶(22.4.5.6)發(fā)來(lái)第二條連接請(qǐng)求,端口為 6666混埠,那么怠缸,新連接的四元組為(11.1.2.3, 8080, 22.4.5.6, 5555)
那么,你主機(jī)的 8080 端口建立了兩條連接钳宪;
通常來(lái)說(shuō)凯旭,服務(wù)端是固定一個(gè)監(jiān)聽(tīng)端口,比如 8080使套,等待客戶端的連接請(qǐng)求罐呼。在不考慮地址重用的情況下,及時(shí) server 端有多個(gè) ip侦高,但是本地監(jiān)聽(tīng)的端口是獨(dú)立的嫉柴。所以對(duì)于 tcp 連接的4元組中,如果destination_ip和destination_port不變奉呛。那么只有source_ip和source_port是可變的计螺,因此最大的 tcp 連接數(shù)應(yīng)該為 客戶端的 ip 數(shù) 乘以 客戶端的端口數(shù)夯尽。在 IPV4 中,不考慮 ip 分類(lèi)等因素登馒,最大的 ip 數(shù)為 2 的 32 次方 匙握;客戶端最大的端口數(shù)為 2 的 16 次方,也就是 65536. 也就是服務(wù)端單機(jī)最大的 tcp 連接數(shù)約為 2 的 48 次方陈轿。
當(dāng)然圈纺,這只是一個(gè)理論值,以 linux 服務(wù)器為例麦射,實(shí)際的連接數(shù)還取決于
- 內(nèi)存大卸耆ⅰ(因?yàn)槊總€(gè) TCP 連接都要占用一定的內(nèi)存)
- 文件句柄限制,每一個(gè) tcp 連接都需要占一個(gè)文件描述符潜秋,一旦這個(gè)文件描述符使用完了蛔琅,新來(lái)的連接會(huì)返回一個(gè)“Can’t open so many files”的異常。如果大家知道對(duì)于操作系統(tǒng)最大可以打開(kāi)的文件數(shù)限制峻呛,就知道怎么去調(diào)整這個(gè)限制
a) 可以執(zhí)行【ulimit -n】得到當(dāng)前一個(gè)進(jìn)程最大能打開(kāi) 1024 個(gè)文件罗售,所以你要采用此默認(rèn)配置最多也就可以并發(fā)上千個(gè) TCP 連接。
b) 可以通過(guò)【vim /etc/security/limits.conf】去修改系統(tǒng)最大文件打開(kāi)數(shù)的限制
soft nofile 2048
hard nofile 2048
表示修改所有用戶限制钩述、soft/hard 表示軟限制還是硬限制寨躁,2048 表示修改以后的值
c) 可以通過(guò)【cat /proc/sys/fs/file-max】查看 linux 系統(tǒng)級(jí)最大打開(kāi)文件數(shù)限制,表示當(dāng)前這個(gè)服務(wù)器最多能同時(shí)打開(kāi)多少個(gè)文件
- 帶寬資源的限制
——學(xué)自咕泡學(xué)院