遠程通訊協議原理

一個 http 請求的整個流程:

在分佈式架構中,有一個很重要的環節,就是分佈式網絡中的計算機節點彼此之間需要通信。這個通信的過程一定會涉及到通信協議相關的知識點,當然大家也可能知道一些,但 是我會盡可能的把通信這一塊的內容串起來,加深大家的理解。 我們每天都在用瀏覽器訪問各種網站,作爲用戶來說,只需要需要輸入一個網址並且正確跳 轉就行。但是作爲程序員,看到的可能就是這個響應背後的整體流程。所以我想通過一個 http 請求的整個流程來進行講解通信的知識。

負責域名解析的 DNS 服務

首先,用戶訪問一個域名,會經過 DNS 解析
DNS(Domain Name System),它和 HTTP 協議一樣是位於應用層的協議,主要提供域名到 IP 的解析服務。我們其實不用域名也可以訪問目標主機的服務,但是 IP 本身不是那麼容易 記,所以使用域名進行替換使得用戶更容易記住。

加速靜態內容訪問速度的 CDN

我這裏只講了比較簡單的解析流程,在很多大型網站,會引入 CDN 來加速靜態內容的訪問, 這裏簡單給大家解釋一下什麼是 CDN(Content Delivery Network),表示的是內容分發網 絡。CDN 其實就是一種網絡緩存技術,能夠把一些相對穩定的資源放到距離最終用戶較近的 地方,一方面可以節省整個廣域網的帶寬消耗,另外一方面可以提升用戶的訪問速度,改進 用戶體驗。我們一般會把靜態的文件(圖片、腳本、靜態頁面)放到 CDN 中。如果引入了 CDN,那麼解析的流程可能會稍微複雜一點,大家有空自己去了解一下。比如阿 裏雲就提供了 cdn 的功能。

HTTP 協議通信原理

域名被成功解析以後,客戶端和服務端之間,是怎麼建立連接並且如何通信的呢? 說到通信,大家一定聽過 tcp 和 udp 這兩種通信協議,以及建立連接的握手過程。而 http 協 議的通信是基於 tcp/ip 協議之上的一個應用層協議,應用層協議除了 http 還有哪些呢(FTP、 DNS、SMTP、Telnet 等)。

涉及到網絡協議,我們一定需要知道 OSI 七層網絡模型和 TCP/IP 四層概念模型,OSI 七層網 絡模型包含(應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層、物理層)、TCP/IP 四 層概念模型包含(應用層、傳輸層、網絡層、數據鏈路層)。

請求發起過程,在 tcp/ip 四層網絡模型中所做的事情:

當應用程序用 T C P 傳送數據時,數據被送入協議棧中,然後逐個通過每一層直到被當作一 串比特流送入網絡。其中每一層對收到的數據都要增加一些首部信息(有時還要增加尾部信 息)

客戶端如何找到目標服務:

在客戶端發起請求的時候,我們會在數據鏈路層去組裝目標機器的 MAC 地址,目標機器的 mac 地址怎麼得到呢? 這裏就涉及到一個 ARP 協議,這個協議簡單來說就是已知目標機器 的 ip,需要獲得目標機器的 mac 地址。(發送一個廣播消息,這個 ip 是誰的,請來認領。認 領 ip 的機器會發送一個 mac 地址的響應)

有了這個目標 MAC 地址,數據包在鏈路上廣播,MAC 的網卡才能發現,這個包是給它的。 MAC 的網卡把包收進來,然後打開 IP 包,發現 IP 地址也是自己的,再打開 TCP 包,發 現端口是自己,也就是 80 端口,而這個時候這臺機器上有一個 nginx 是監聽 80 端口。 於是將請求提交給 nginx,nginx 返回一個網頁。然後將網頁需要發回請求的機器。然後層層 封裝,最後到 MAC 層。因爲來的時候有源 MAC 地址,返回的時候,源 MAC 就變成了目 標 MAC,再返給請求的機器。

爲了避免每次都用 ARP 請求,機器本地也會進行 ARP 緩存。當然機器會不斷地上線下線, IP 也可能會變,所以 ARP 的 MAC 地址緩存過一段時間就會過期。

接收端收到數據包以後的處理過程:
當目的主機收到一個以太網數據幀時,數據就開始從協議棧中由底向上升,同時去掉各層協 議加上的報文首部。每層協議都要去檢查報文首部中的協議標識,以確定接收數據的上層協 議。

爲什麼有了 MAC 層還要走 IP 層呢?

之前我們提到,mac 地址是唯一的,那理論上,在任何兩個設備之間,我應該都可以通過 mac 地址發送數據,爲什麼還需要 ip 地址?
mac 地址就好像個人的身份證號,人的身份證號和人戶口所在的城市,出生的日期有關, 但是和人所在的位置沒有關係,人是會移動的,知道一個人的身份證號,並不能找到它這個 人,mac 地址類似,它是和設備的生產者,批次,日期之類的關聯起來,知道一個設備的 mac,並不能在網絡中將數據發送給它,除非它和發送方的在同一個網絡內。 所以要實現機器之間的通信,我們還需要有 ip 地址的概念,ip 地址表達的是當前機器在網 絡中的位置,類似於城市名+道路號+門牌號的概念。通過 ip 層的尋址,我們能知道按何種 路徑在全世界任意兩臺 Internet 上的的機器間傳輸數據。

TCP/IP 的分層管理:

TCP/IP 協議按照層次分爲 4 層:應用層、傳輸層、網絡層、數據鏈路層。對於分層這個概念, 大家一定不陌生,比如我們的分佈式架構體系中會分爲業務層、服務層、基礎支撐層。比如 docker,也是基於分層來實現。所以我們會發現,複雜的程序都需要分層,這個是軟件設計 的要求,每一層專注於當前領域的事情。如果某些地方需要修改,我們只需要把變動的層替 換掉就行,一方面改動影響較少,另一方面整個架構的靈活性也更高。 最後,在分層之後, 整個架構的設計也變得相對簡單了。

TCP/IP 協議的深入分析

通過前面一個案例的分析,基本清楚了網絡的通信流程,在 http 協議中,底層用到了 tcp 的

通信協議,我們接下來給大家簡單介紹一下 tcp 的通信協議原理。 我們如果需要深入學習網絡協議,就要先把一些基本的協議的作用和工作過程搞清楚,網絡 設備還沒智能到人腦的程度,它是由人類創造出來的,它的工作過程肯定是符合人類的交流 習慣並按照人類的交流習慣來設計的。所以要以人類的思維方式去理解這些協議。 例如,你給別人打電話,不可能電話一接通你就啪啦啪啦地說一大通,萬一對方接通電話後 因爲有事還沒來得及傾聽呢?這不太符合正常人類的交流習慣。一般是電話接通後,雙方會 有個交互的過程,會先說一聲“你好”,然後對方也回覆一聲“你好”,雙方通過各自一句“你好” 明確對方的注意力都放在了電話溝通上,然後你們雙方就可以開始交流了,這纔是正常的人 類交流方式,這個過程體現在計算機網絡裏就是網絡協議!我們通過 TCP 協議在兩臺電腦建 立網絡連接之前要先發數據包進行溝通,溝通後再建立連接,然後纔是信息的傳輸。而 UDP 協議就類似於我們的校園廣播,廣播內容已經通過廣播站播放出去了,你能不能聽到,那就 與廣播站無關了,正常情況下,不可能你說沒注意聽然後再讓廣播站再播放一次廣播內容。 基於這些思路,我們先去了解下 TCP 裏面關注比較多的握手協議

TCP 握手協議

所以 TCP 消息的可靠性首先來自於有效的連接建立,所以在數據進行傳輸前,需要通過三次 握手建立一個連接,所謂的三次握手,就是在建立 TCP 鏈接時,需要客戶端和服務端總共發 送 3 個包來確認連接的建立,在 socket 編程中,這個過程由客戶端執行 connect 來觸發

解析:

 

第一次握手 (SYN=1, seq=x) 客戶端發送一個 TCP 的 SYN 標誌 位置 1 的包,指明客 戶端打算連接的服 務器的端口,以及 初始序號 X,保存 在包頭的序列號 (Sequence Number)字段裏。 發送完畢後,客戶 端進入 SYN_SEND 狀態。 第二次握手

(SYN=1, ACK=1, seq=y, ACKnum=x+1): 服務器發回確認包 (ACK) 應 答 。 即 SYN 標誌位和 ACK 標誌位均爲 1。服務器端選擇自 己 ISN 序列號,放 到 Seq 域裏,同時 將確認序號 (Acknowledgeme nt Number)設置爲 客戶的 ISN 加 1, 即 X+1。 發送完畢後,服務 器端進入 SYN_RCVD 狀態。 第三次握手 (ACK=1 , ACKnum=y+1) 客戶端再次發送確 認包(ACK),SYN 標 志位爲 0,ACK 標 志位爲 1,並且把服 務器發來 ACK 的 序號字段+1,放在 確定字段中發送給 對方,並且在數據 段放寫 ISN 發完畢 後,客戶端進入 ESTABLISHED 狀 態,當服務器端接收到這個包時,也 進入 ESTABLISHED 狀 態,TCP 握手結束。

那 TCP 在三次握手的時候,IP 層和 MAC 層在做什麼呢?當然是 TCP 發送每一個消息, 都會帶着 IP 層和 MAC 層了。因爲,TCP 每發送一個消息,IP 層和 MAC 層的所有機制都 要運行一遍。而你只看到 TCP 三次握手了,其實,IP 層和 MAC 層爲此也忙活好久了。

TCP 四次揮手協議:

四次揮手錶示 TCP 斷開連接的時候,需要客戶端和服務端總共發送 4 個包以確認連接的斷開; 客戶端或服務器均可主動發起揮手動作(因爲 TCP 是一個全雙工協議),在 socket 編程中, 任何一方執行 close() 操作即可產生揮手操作。

單工:數據傳輸只支持數據在一個方 向上傳輸 半雙工:數據傳輸允許數據在兩個方 向上傳輸,但是在某一時刻,只允許 在一個方向上傳輸,實際上有點像切 換方向的單工通信

全雙工:數據通信允許數據同時在兩 個方向上傳輸,因此全雙工是兩個單 工通信方式的結合,它要求發送設備 和接收設備都有獨立的接收和發送 能力

第一次揮手(FIN=1,seq=x)
假設客戶端想要關閉連接,客戶端發送一個 FIN 標誌位置爲 1 的包,表示自己已經沒有數據 可以發送了,但是仍然可以接受數據。發送完畢後,客戶端進入 FIN_WAIT_1 狀態。 第二次揮手(ACK=1,ACKnum=x+1)
服務器端確認客戶端的 FIN 包,發送一個確認包,表明自己接受到了客戶端關閉連接的請求, 但還沒有準備好關閉連接。發送完畢後,服務器端進入 CLOSE_WAIT 狀態,客戶端接收到這 個確認包之後,進入 FIN_WAIT_2 狀態,等待服務器端關閉連接。 第三次揮手(FIN=1,seq=w)
服務器端準備好關閉連接時,向客戶端發送結束連接請求,FIN 置爲 1。發送完畢後,服務器 端進入 LAST_ACK 狀態,等待來自客戶端的最後一個 ACK。 第四次揮手(ACK=1,ACKnum=w+1) 客戶端接收到來自服務器端的關閉請求,發送一個確認包,並進入 TIME_WAIT 狀態,等待 可能出現的要求重傳的 ACK 包。
服務器端接收到這個確認包之後,關閉連接,進入 CLOSED 狀態。 客戶端等待了某個固定時間(兩個最大段生命週期,2MSL,2 Maximum Segment Lifetime) 之後,沒有收到服務器端的 ACK,認爲服務器端已經正常關閉連接,於是自己也關閉連接, 進入 CLOSED 狀態。

假設 Client 端發起中斷連接請求,也就是發送 FIN 報文。Server 端接到 FIN 報文後,意思是 說"我 Client 端沒有數據要發給你了",但是如果你還有數據沒有發送完成,則不必急着關閉 Socket,可以繼續發送數據。所以你先發送 ACK,"告訴 Client 端,你的請求我收到了,但是 我還沒準備好,請繼續你等我的消息"。這個時候 Client 端就進入 FIN_WAIT 狀態,繼續等待 Server 端的 FIN 報文。當 Server 端確定數據已發送完成,則向 Client 端發送 FIN 報文,"告 訴 Client 端,好了,我這邊數據發完了,準備好關閉連接了"。Client 端收到 FIN 報文後,"就 知道可以關閉連接了,但是他還是不相信網絡,怕 Server 端不知道要關閉,所以發送 ACK 後 進入 TIME_WAIT 狀態,如果 Server 端沒有收到 ACK 則可以重傳。“,Server 端收到 ACK 後, "就知道可以斷開連接了"。Client 端等待了 2MSL 後依然沒有收到回覆,則證明 Server 端已 正常關閉,那好,我 Client 端也可以關閉連接了。Ok,TCP 連接就這樣關閉了!

疑問:

【問題 1】爲什麼連接的時候是三次握手,關閉的時候卻是四次握手? 答:三次握手是因爲因爲當 Server 端收到 Client 端的 SYN 連接請求報文後,可以直接發送 SYN+ACK 報文。其中 ACK 報文是用來應答的,SYN 報文是用來同步的。但是關閉連接時, 當 Server 端收到 FIN 報文時,很可能並不會立即關閉 SOCKET(因爲可能還有消息沒處理完),所以只能先回復一個 ACK 報文,告訴 Client 端,"你發的 FIN 報文我收到了"。只有等到 我 Server 端所有的報文都發送完了,我才能發送 FIN 報文,因此不能一起發送。故需要四步 握手。

【問題 2】爲什麼 TIME_WAIT 狀態需要經過 2MSL(最大報文段生存時間)才能返回到 CLOSE 狀態?

答:雖然按道理,四個報文都發送完畢,我們可以直接進入 CLOSE 狀態了,但是我們必須假 象網絡是不可靠的,有可以最後一個 ACK 丟失。所以 TIME_WAIT 狀態就是用來重發可能丟 失的 ACK 報文。

使用協議進行通信:

tcp 連接建立以後,就可以基於這個連接通道來發送和接受消息了,TCP、UDP 都是在基於 Socket 概念上爲某類應用場景而擴展出的傳輸協議,那麼什麼是 socket 呢?socket 是一種 抽象層,應用程序通過它來發送和接收數據,就像應用程序打開一個文件句柄,把數據讀寫 到磁盤上一樣。使用 socket 可以把應用程序添加到網絡中,並與處於同一個網絡中的其他應 用程序進行通信。不同類型的 Socket 與不同類型的底層協議簇有關聯。主要的 socket 類型 爲流套接字(stream socket)和數據報文套接字(datagram socket)。 stream socket 把 TCP 作爲端對端協議(底層使用 IP 協議),提供一個可信賴的字節流服務。數據報文套接字 (datagram socket)使用 UDP 協議(底層同樣使用 IP 協議)提供了一種“盡力而爲”的數據 報文服務。

接下來,我們使用 Java 提供的 API 來展示 TCP 協議的客戶端和服務端通信的案例和 UDP 協議的客戶端和服務端通信的案例,然後更進一步瞭解底層的原理

單工版:

全工版:

我們通過一個圖來簡單描述一下 socket 鏈接建立以及通信的模型:

理解 TCP 的通信原理及 IO 阻塞:

通過上面這個簡單的案例,基本清楚了在 Java 應用程序中如何使用 socket 套接字來建立一 個基於 tcp 協議的通信流程。接下來,我們在來了解一下 tcp 的底層通信過程是什麼樣的

瞭解 TCP 協議的通信過程:
首先,對於 TCP 通信來說,每個 TCP Socket 的內核中都有一個發送緩衝區和一個接收緩衝 區,TCP 的全雙工的工作模式及 TCP 的滑動窗口就是依賴於這兩個獨立的 Buffer 和該 Buffer 的填充狀態。
接收緩衝區把數據緩存到內核,若應用進程一直沒有調用 Socket 的 read 方法進行讀取,那 麼該數據會一直被緩存在接收緩衝區內。不管進程是否讀取 Socket,對端發來的數據都會經 過內核接收並緩存到 Socket 的內核接收緩衝區。

read 所要做的工作,就是把內核接收緩衝區中的數據複製到應用層用戶的 Buffer 裏。 進程調用 Socket 的 send 發送數據的時候,一般情況下是將數據從應用層用戶的 Buffer 裏復 制到 Socket 的內核發送緩衝區,然後 send 就會在上層返回。換句話說,send 返回時,數據 不一定會被髮送到對端。

前面我們提到,Socket 的接收緩衝區被 TCP 用來緩存網絡上收到的數據,一直保存到應用進程讀走爲止。如果應用進程一直沒有讀取,那麼 Buffer 滿了以後,出現的情況是:通知對端 TCP 協議中的窗口關閉,保證 TCP 接收緩衝區不會移除,保證了 TCP 是可靠傳輸的。如果對 方無視窗口大小發出了超過窗口大小的數據,那麼接收方會把這些數據丟棄。

滑動窗口協議:

這個過程中涉及到了 TCP 的滑動窗口協議,滑動窗口(Sliding window)是一種流量控制技 術。早期的網絡通信中,通信雙方不會考慮網絡的擁擠情況直接發送數據。由於大家不知道 網絡擁塞狀況,同時發送數據,導致中間節點阻塞掉包,誰也發不了數據,所以就有了滑動 窗口機制來解決此問題;發送和接受方都會維護一個數據幀的序列,這個序列被稱作窗口

發送窗口:

就是發送端允許連續發送的幀的序號表。 發送端可以不等待應答而連續發送的最大幀數稱爲發送窗口的尺寸。

接收窗口:

接收方允許接收的幀的序號表,凡落在 接收窗口內的幀,接收方都必須處理,落在接收窗口 外的幀被丟棄。
接收方每次允許接收的幀數稱爲接收窗口的尺寸。
在線滑動窗口演示功能:https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanima tions/selective-repeat-protocol/index.html

理解阻塞到底是什麼回事:

瞭解了基本通信原理以後,我們再來思考一個問題,在前面的代碼演示中,我們通過 socket.accept 去接收一個客戶端請求,accept 是一個阻塞的方法,意味着 TCP 服務器一次 只能處理一個客戶端請求,當一個客戶端向一個已經被其他客戶端佔用的服務器發送連接請 求時,雖然在連接建立後可以向服務端發送數據,但是在服務端處理完之前的請求之前,卻 不會對新的客戶端做出響應,這種類型的服務器稱爲“迭代服務器”。迭代服務器是按照順序處 理客戶端請求,也就是服務端必須要處理完前一個請求才能對下一個客戶端的請求進行響應。 但是在實際應用中,我們不能接收這樣的處理方式。所以我們需要一種方法可以獨立處理每 一個連接,並且他們之間不會相互干擾。而 Java 提供的多線程技術剛好滿足這個需求,這個 機制使得服務器能夠方便處理多個客戶端的請求。

一個客戶端對應一個線程:

爲每個客戶端創建一個線程實際上會存在一些弊端,因爲創建一個線程需要佔用 CPU 的資 源和內存資源。另外,隨着線程數增加,系統資源將會成爲瓶頸最終達到一個不可控的狀 態,所以我們還可以通過線程池來實現多個客戶端請求的功能,因爲線程池是可控的。

非阻塞模型:

上面這種模型雖然優化了 IO 的處理方式,但是,不管是線程池還是單個線程,線程本身的處 理個數是有限制的,對於操作系統來說,如果線程數太多會造成 CPU 上下文切換的開銷。因 此這種方式不能解決根本問題
所以在 Java1.4 以後,引入了 NIO(New IO)的功能,我不希望直接來給大家解釋 NIO 的原 理,我還是會基於 BIO 到 NIO 的過程來帶着大家思考

阻塞 IO:

前面其實已經簡單講過了阻塞 IO 的原理,我想在這裏重申一下什麼是阻塞 IO 呢? 就是當 客戶端的數據從網卡緩衝區複製到內核緩衝區之前,服務端會一直阻塞。以 socket 接口爲例, 進程空間中調用 recvfrom,進程從調用 recvfrom 開始到它返回的整段時間內都是被阻塞的, 因此被成爲阻塞 IO 模型

非阻塞 IO:

那大家思考一個問題,如果我們希望這臺服務器能夠處理更多的連接,怎麼去優化呢? 我們第一時間想到的應該是如何保證這個阻塞變成非阻塞吧。所以就引入了非阻塞 IO 模型, 非阻塞 IO 模型的原理很簡單,就是進程空間調用 recvfrom,如果這個時候內核緩衝區沒有 數據的話,就直接返回一個 EWOULDBLOCK 錯誤,然後應用程序通過不斷輪詢來檢查這個 狀態狀態,看內核是不是有數據過 來。

I/O 複用模型:

我們前面講的非阻塞仍然需要進程不斷的輪詢重試。能不能實現當數據可讀了以後給程序一 個通知呢?所以這裏引入了一個 IO 多路複用模型,I/O 多路複用的本質是通過一種機制(系 統內核緩衝 I/O 數據),讓單個進程可以監視多個文件描述符,一旦某個描述符就緒(一般是 讀就緒或寫就緒),能夠通知程序進行相應的讀寫操作

【什麼是 fd:在 linux 中,內核把所有的外部設備都當成是一個文件來操作,對一個文件的讀 寫會調用內核提供的系統命令,返回一個 fd(文件描述符)。而對於一個 socket 的讀寫也會有 相應的文件描述符,成爲 socketfd】

常見的 IO 多路複用方式有【select、poll、epoll】,都是 Linux API 提供的 IO 複用方式,那麼接下來重點講一下 select、和 epoll 這兩個模型:

select:進程可以通過把一個或者多個 fd 傳遞給 select 系統調用,進程會阻塞在 select 操作 上,這樣 select 可以幫我們檢測多個 fd 是否處於就緒狀態。
這個模式有二個缺點

  1. 由於他能夠同時監聽多個文件描述符,假如說有 1000 個,這個時候如果其中一個 fd 處於

    就緒狀態了,那麼當前進程需要線性輪詢所有的 fd,也就是監聽的 fd 越多,性能開銷越

    大。

  2. 同時,select 在單個進程中能打開的 fd 是有限制的,默認是 1024,對於那些需要支持單機

    上萬的 TCP 連接來說確實有點少

epoll:linux 還提供了 epoll 的系統調用,epoll 是基於事件驅動方式來代替順序掃描,因此性 能相對來說更高,主要原理是,當被監聽的 fd 中,有 fd 就緒時,會告知當前進程具體哪一 個 fd 就緒,那麼當前進程只需要去從指定的 fd 上讀取數據即可
另外,epoll 所能支持的 fd 上線是操作系統的最大文件句柄,這個數字要遠遠大於 1024 【由於 epoll 能夠通過事件告知應用進程哪個 fd 是可讀的,所以我們也稱這種 IO 爲異步非 阻塞 IO,當然它是僞異步的,因爲它還需要去把數據從內核同步複製到用戶空間中,真正的 異步非阻塞,應該是數據已經完全準備好了,我只需要從用戶空間讀就行】

多路複用的好處:

I/O 多路複用可以通過把多個 I/O 的阻塞複用到同一個 select 的阻塞上,從而使得系統在單 線程的情況下可以同時處理多個客戶端請求。它的最大優勢是系統開銷小,並且不需要創建 新的進程或者線程,降低了系統的資源開銷

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章