遠程通信協(xié)議 http
一個 http 請求的整個流程
在分布式架構中,有一個很重要的環(huán)節(jié),就是分布式網絡中的計算機節(jié)點彼此之間需要通信。這個通信的過程一定會涉及到通信協(xié)議相關的知識點,當然大家也可能知道一些,但
是我會盡可能的把通信這一塊的內容串起來,加深大家的理解。我們每天都在用瀏覽器訪問各種網站,作為用戶來說,只需要需要輸入一個網址并且正確跳轉就行。但是作為程序員,看到的可能就是這個響應背后的整體流程。所以我想通過一個 http請求的整個流程來進行講解通信的知識
負責域名解析的 DNS 服務
首先,用戶訪問一個域名,會經過 DNS 解析DNS(Domain Name System),它和 HTTP 協(xié)議一樣是位于應用層的協(xié)議,主要提供域名到IP 的解析服務。我們其實不用域名也可以訪問目標主機的服務,但是 IP 本身不是那么容易記,所以使用域名進行替換使得用戶更容易記住。
加速靜態(tài)內容訪問速度的 CDN
我這里只講了比較簡單的解析流程,在很多大型網站,會引入 CDN 來加速靜態(tài)內容的訪問,這里簡單給大家解釋一下什么是 CDN(Content Delivery Network),表示的是內容分發(fā)網絡。CDN 其實就是一種網絡緩存技術,能夠把一些相對穩(wěn)定的資源放到距離最終用戶較近的地方,一方面可以節(jié)省整個廣域網的帶寬消耗,另外一方面可以提升用戶的訪問速度,改進用戶體驗。我們一般會把靜態(tài)的文件(圖片、腳本、靜態(tài)頁面)放到 CDN 中。如果引入了 CDN,那么解析的流程可能會稍微復雜一點,大家有空自己去了解一下。比如阿里云就提供了 cdn 的功能。
HTTP 協(xié)議通信原理
域名被成功解析以后,客戶端和服務端之間,是怎么建立連接并且如何通信的呢?說到通信,大家一定聽過 tcp 和 udp 這兩種通信協(xié)議,以及建立連接的握手過程。而 http 協(xié)議的通信是基于 tcp/ip 協(xié)議之上的一個應用層協(xié)議,應用層協(xié)議除了 http 還有哪些呢(FTP、DNS、SMTP、Telnet 等)。涉及到網絡協(xié)議,我們一定需要知道 OSI 七層網絡模型和 TCP/IP 四層概念模型,
OSI 七層網絡模型包含(應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層、物理層)
TCP/IP 四層概念模型包含(應用層、傳輸層、網絡層、數據鏈路層)。
請求發(fā)起過程,在 tcp/ip 四層網絡模型中所做的事情當應用程序用 TCP 傳送數據時,數據被送入協(xié)議棧中,然后逐個通過每一層直到被當作一串比特流送入網絡。其中每一層對收到的數據都要增加一些首部信息(有時還要增加尾部信息)
客戶端如何找到目標服務
在客戶端發(fā)起請求的時候,我們會在數據鏈路層去組裝目標機器的 MAC 地址,目標機器的mac 地址怎么得到呢? 這里就涉及到一個 ARP 協(xié)議,這個協(xié)議簡單來說就是已知目標機器的 ip,需要獲得目標機器的 mac 地址。(發(fā)送一個廣播消息,這個 ip 是誰的,請來認領。認領 ip 的機器會發(fā)送一個 mac 地址的響應)有了這個目標 MAC 地址,數據包在鏈路上廣播,MAC 的網卡才能發(fā)現,這個包是給它的。MAC 的網卡把包收進來,然后打開 IP 包,發(fā)現 IP 地址也是自己的,再打開 TCP 包,發(fā)現端口是自己,也就是 80 端口,而這個時候這臺機器上有一個 nginx 是監(jiān)聽 80 端口。于是將請求提交給 nginx,nginx 返回一個網頁。然后將網頁需要發(fā)回請求的機器。然后層層封裝,最后到 MAC 層。因為來的時候有源 MAC 地址,返回的時候,源 MAC 就變成了目標 MAC,再返給請求的機器。
為了避免每次都用 ARP 請求,機器本地也會進行 ARP 緩存。當然機器會不斷地上線下線,IP 也可能會變,所以 ARP 的 MAC 地址緩存過一段時間就會過期。
接收端收到數據包以后的處理過程
當目的主機收到一個以太網數據幀時,數據就開始從協(xié)議棧中由底向上升,同時去掉各層協(xié)議加上的報文首部。每層協(xié)議都要去檢查報文首部中的協(xié)議標識,以確定接收數據的上層協(xié)議

為什么有了 MAC 層還要走 IP 層呢?
之前我們提到,mac 地址是唯一的,那理論上,在任何兩個設備之間,我應該都可以通過mac 地址發(fā)送數據,為什么還需要 ip 地址?
mac 地址就好像個人的身份證號,人的身份證號和人戶口所在的城市,出生的日期有關,但是和人所在的位置沒有關系,人是會移動的,知道一個人的身份證號,并不能找到它這個
人,mac 地址類似,它是和設備的生產者,批次,日期之類的關聯(lián)起來,知道一個設備的mac,并不能在網絡中將數據發(fā)送給它,除非它和發(fā)送方的在同一個網絡內。所以要實現機器之間的通信,我們還需要有 ip 地址的概念,ip 地址表達的是當前機器在網絡中的位置,類似于城市名+道路號+門牌號的概念。通過 ip 層的尋址,我們能知道按何種路徑在全世界任意兩臺 Internet 上的的機器間傳輸數據。
TCP/IP 的分層管理
TCP/IP 協(xié)議按照層次分為 4 層:應用層、傳輸層、網絡層、數據鏈路層。對于分層這個概念,大家一定不陌生,比如我們的分布式架構體系中會分為業(yè)務層、服務層、基礎支撐層。比如docker,也是基于分層來實現。所以我們會發(fā)現,復雜的程序都需要分層,這個是軟件設計的要求,每一層專注于當前領域的事情。如果某些地方需要修改,我們只需要把變動的層替換掉就行,一方面改動影響較少,另一方面整個架構的靈活性也更高。 最后,在分層之后,整個架構的設計也變得相對簡單了。
分層負載
了解了分層的概念以后,我們再去理解所謂的二層負載、三層負載、四層負載、七層負載就容易多了。
一次 http 請求過來,一定會從應用層到傳輸層,完成整個交互。只要是在網絡上跑的數據包,都是完整的。可以有下層沒上層,絕對不可能有上層沒下層。
二層負載
二層負載是針對 MAC,負載均衡服務器對外依然提供一個 VIP(虛 IP),集群中不同的機器采用相同 IP 地址,但是機器的 MAC 地址不一樣。當負載均衡服務器接受到請求之后,通過改寫報文的目標 MAC 地址的方式將請求轉發(fā)到目標機器實現負載均衡。二層負載均衡會通過一個虛擬 MAC 地址接收請求,然后再分配到真實的 MAC 地址
三層負載均衡
三層負載是針對 IP,和二層負載均衡類似,負載均衡服務器對外依然提供一個 VIP(虛 IP),但是集群中不同的機器采用不同的 IP 地址。當負載均衡服務器接受到請求之后,根據不同的負載均衡算法,通過 IP 將請求轉發(fā)至不同的真實服務器。三層負載均衡會通過一個虛擬 IP 地址接收請求,然后再分配到真實的 IP 地址
四層負載均衡
四層負載均衡工作在 OSI 模型的傳輸層,由于在傳輸層,只有 TCP/UDP 協(xié)議,這兩種協(xié)議中除了包含源 IP、目標 IP 以外,還包含源端口號及目的端口號。四層負載均衡服務器在接受到客戶端請求后,以后通過修改數據包的地址信息(IP+端口號)將流量轉發(fā)到應用服務器。四層通過虛擬 IP + 端口接收請求,然后再分配到真實的服務器
七層負載均衡
七層負載均衡工作在 OSI 模型的應用層,應用層協(xié)議較多,常用 http、radius、dns 等。七層負載就可以基于這些協(xié)議來負載。這些應用層協(xié)議中會包含很多有意義的內容。比如同一個Web 服務器的負載均衡,除了根據 IP 加端口進行負載外,還可根據七層的 URL、瀏覽器類別來決定是否要進行負載均衡七層通過虛擬的 URL 或主機名接收請求,然后再分配到真實的服務器
TCP/IP 協(xié)議的深入分析
TCP/IP 協(xié)議的深入分析
通過前面一個案例的分析,基本清楚了網絡的通信流程,在 http 協(xié)議中,底層用到了 tcp 的通信協(xié)議,我們接下來給大家簡單介紹一下 tcp 的通信協(xié)議原理。我們如果需要深入學習網絡協(xié)議,就要先把一些基本的協(xié)議的作用和工作過程搞清楚,網絡設備還沒智能到人腦的程度,它是由人類創(chuàng)造出來的,它的工作過程肯定是符合人類的交流習慣并按照人類的交流習慣來設計的。所以要以人類的思維方式去理解這些協(xié)議。
例如,你給別人打電話,不可能電話一接通你就啪啦啪啦地說一大通,萬一對方接通電話后因為有事還沒來得及傾聽呢?這不太符合正常人類的交流習慣。一般是電話接通后,雙方會有個交互的過程,會先說一聲“你好”,然后對方也回復一聲“你好”,雙方通過各自一句“你好”明確對方的注意力都放在了電話溝通上,然后你們雙方就可以開始交流了,這才是正常的人類交流方式,這個過程體現在計算機網絡里就是網絡協(xié)議!我們通過 TCP 協(xié)議在兩臺電腦建立網絡連接之前要先發(fā)數據包進行溝通,溝通后再建立連接,然后才是信息的傳輸。而 UDP協(xié)議就類似于我們的校園廣播,廣播內容已經通過廣播站播放出去了,你能不能聽到,那就與廣播站無關了,正常情況下,不可能你說沒注意聽然后再讓廣播站再播放一次廣播內容。基于這些思路,我們先去了解下 TCP 里面關注比較多的握手協(xié)議
TCP 握手協(xié)議
所以 TCP 消息的可靠性首先來自于有效的連接建立,所以在數據進行傳輸前,需要通過三次握手建立一個連接,所謂的三次握手,就是在建立 TCP 鏈接時,需要客戶端和服務端總共發(fā)送 3 個包來確認連接的建立,在 socket 編程中,這個過程由客戶端執(zhí)行 connect 來觸發(fā)
(SYN=1, ACK=1,seq=y,ACKnum=x+1):服務器發(fā)回確認包(ACK) 應 答 。 即SYN 標志位和ACK 標 志 位 均 為1。服務器端選擇自己 ISN 序列號,放到 Seq 域里,同時將 確 認 序 號(Acknowledgement Number)設置為客戶的 ISN 加 1,即 X+1。發(fā)送完畢后,服務器 端 進 入SYN_RCVD 狀態(tài)。
第 三 次 握 手(ACK=1,ACKnum=y+1)客戶端再次發(fā)送確認包(ACK),SYN 標志位為 0,ACK 標志位為 1,并且把服務器發(fā)來 ACK 的序號字段+1,放在確定字段中發(fā)送給對方,并且在數據段放寫 ISN 發(fā)完畢后 , 客 戶 端 進 入ESTABLISHED 狀態(tài),當服務器端接收到這個包時,也進 入ESTABLISHED 狀態(tài),TCP 握手結束。
SYN 攻擊
在三次握手過程中,Server 發(fā)送 SYN-ACK 之后,收到 Client 的 ACK 之前的 TCP 連接稱為半連接(half-open connect),此時 Server 處于 SYN_RCVD 狀態(tài),當收到 ACK 后,Server轉入 ESTABLISHED 狀態(tài)。SYN 攻擊就是 Client 在短時間內偽造大量不存在的 IP 地址,并向Server 不斷地發(fā)送 SYN 包,Server 回復確認包,并等待 Client 的確認,由于源地址是不存在的,因此,Server 需要不斷重發(fā)直至超時,這些偽造的 SYN 包將產時間占用未連接隊列,導致正常的 SYN 請求因為隊列滿而被丟棄,從而引起網絡堵塞甚至系統(tǒng)癱瘓。SYN 攻擊時一種典型的 DDOS 攻擊,檢測 SYN 攻擊的方式非常簡單,即當 Server 上有大量半連接狀態(tài)且源 IP 地址是隨機的,則可以斷定遭到 SYN 攻擊了
TCP 四次揮手協(xié)議
四次揮手表示 TCP 斷開連接的時候,需要客戶端和服務端總共發(fā)送 4 個包以確認連接的斷開;客戶端或服務器均可主動發(fā)起揮手動作(因為 TCP 是一個全雙工協(xié)議),在 socket 編程中,任何一方執(zhí)行 close() 操作即可產生揮手操作。
第一次揮手(FIN=1,seq=x)
假設客戶端想要關閉連接,客戶端發(fā)送一個 FIN 標志位置為 1 的包,表示自己已經沒有數據可以發(fā)送了,但是仍然可以接受數據。發(fā)送完畢后,客戶端進入 FIN_WAIT_1 狀態(tài)。
第二次揮手(ACK=1,ACKnum=x+1)
服務器端確認客戶端的 FIN 包,發(fā)送一個確認包,表明自己接受到了客戶端關閉連接的請求,但還沒有準備好關閉連接。發(fā)送完畢后,服務器端進入 CLOSE_WAIT 狀態(tài),客戶端接收到這個確認包之后,進入 FIN_WAIT_2 狀態(tài),等待服務器端關閉連接。
第三次揮手(FIN=1,seq=w)
服務器端準備好關閉連接時,向客戶端發(fā)送結束連接請求,FIN 置為 1。發(fā)送完畢后,服務器端進入 LAST_ACK 狀態(tài),等待來自客戶端的最后一個 ACK。
第四次揮手(ACK=1,ACKnum=w+1)
客戶端接收到來自服務器端的關閉請求,發(fā)送一個確認包,并進入 TIME_WAIT 狀態(tài),等待可能出現的要求重傳的 ACK 包。服務器端接收到這個確認包之后,關閉連接,進入 CLOSED 狀態(tài)??蛻舳说却四硞€固定時間(兩個最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,沒有收到服務器端的 ACK,認為服務器端已經正常關閉連接,于是自己也關閉連接,進入 CLOSED 狀態(tài)。假設 Client 端發(fā)起中斷連接請求,也就是發(fā)送 FIN 報文。Server 端接到 FIN 報文后,意思是說"我 Client 端沒有數據要發(fā)給你了",但是如果你還有數據沒有發(fā)送完成,則不必急著關閉Socket,可以繼續(xù)發(fā)送數據。所以你先發(fā)送 ACK,"告訴 Client 端,你的請求我收到了,但是我還沒準備好,請繼續(xù)你等我的消息"。這個時候 Client 端就進入 FIN_WAIT 狀態(tài),繼續(xù)等待Server 端的 FIN 報文。當 Server 端確定數據已發(fā)送完成,則向 Client 端發(fā)送 FIN 報文,"告訴 Client 端,好了,我這邊數據發(fā)完了,準備好關閉連接了"。Client 端收到 FIN 報文后,"就知道可以關閉連接了,但是他還是不相信網絡,怕 Server 端不知道要關閉,所以發(fā)送 ACK 后進入 TIME_WAIT 狀態(tài),如果 Server 端沒有收到 ACK 則可以重傳。“,Server 端收到 ACK 后,"就知道可以斷開連接了"。Client 端等待了 2MSL 后依然沒有收到回復,則證明 Server 端已正常關閉,那好,我 Client 端也可以關閉連接了。Ok,TCP 連接就這樣關閉了!
問題
【問題 1】為什么連接的時候是三次握手,關閉的時候卻是四次握手?
答:三次握手是因為因為當 Server 端收到 Client 端的 SYN 連接請求報文后,可以直接發(fā)送SYN+ACK 報文。其中 ACK 報文是用來應答的,SYN 報文是用來同步的。但是關閉連接時,當 Server 端收到 FIN 報文時,很可能并不會立即關閉 SOCKET(因為可能還有消息沒處理完),所以只能先回復一個 ACK 報文,告訴 Client 端,"你發(fā)的 FIN 報文我收到了"。只有等到我 Server 端所有的報文都發(fā)送完了,我才能發(fā)送 FIN 報文,因此不能一起發(fā)送。故需要四步握手。
【問題 2】為什么 TIME_WAIT 狀態(tài)需要經過 2MSL(最大報文段生存時間)才能返回到 CLOSE狀態(tài)?
答:雖然按道理,四個報文都發(fā)送完畢,我們可以直接進入 CLOSE 狀態(tài)了,但是我們必須假象網絡是不可靠的,有可以最后一個 ACK 丟失。所以 TIME_WAIT 狀態(tài)就是用來重發(fā)可能丟失的 ACK 報文。
使用協(xié)議進行通信
tcp 連接建立以后,就可以基于這個連接通道來發(fā)送和接受消息了,TCP、UDP 都是在基于Socket 概念上為某類應用場景而擴展出的傳輸協(xié)議,那么什么是 socket 呢?socket 是一種抽象層,應用程序通過它來發(fā)送和接收數據,就像應用程序打開一個文件句柄,把數據讀寫到磁盤上一樣。使用 socket 可以把應用程序添加到網絡中,并與處于同一個網絡中的其他應用程序進行通信。不同類型的 Socket 與不同類型的底層協(xié)議簇有關聯(lián)。主要的 socket 類型為流套接字(stream socket)和數據報文套接字(datagram socket)。 stream socket 把 TCP作為端對端協(xié)議(底層使用 IP 協(xié)議),提供一個可信賴的字節(jié)流服務。數據報文套接字(datagram socket)使用 UDP 協(xié)議(底層同樣使用 IP 協(xié)議)提供了一種“盡力而為”的數據報文服務
接下來,我們使用 Java 提供的 API 來展示 TCP 協(xié)議的客戶端和服務端通信的案例和 UDP協(xié)議的客戶端和服務端通信的案例,然后更進一步了解底層的原理
基于 TCP 協(xié)議實現通信
實現一個簡單的從客戶端發(fā)送一個消息到服務端的功能
服務端
public static void main(String[] args)throws IOException { ServerSocket serverSocket=null; BufferedReader in=null; try{ //TCP 的服務端要先監(jiān)聽一個端口,一般是先調用bind 函數,給這個 Socket 賦予一個 IP 地址和端口。為什么需要端口呢?要知道,你寫的是一個應用程序,當一個網絡包來的時候,內核要通過 TCP 頭里 面的這個端口,來找到你這個應用程序,把包給你。為什么要 IP 地址呢?有時候,一臺機器會有多個網卡,也就會有多個 IP 地址,你可以選擇監(jiān)聽所有的網卡,也可以選擇監(jiān)聽一個網卡,這樣,只有發(fā)給這 個網卡的包,才會給你。 serverSocket=new ServerSocket(8080); //阻塞等待客戶端連接,接下來,服務端調用 accept 函數,拿出一個已經完成的連接進行處理。如果還沒有完成,就要等著。 Socket socket=serverSocket.accept(); //連接建立成功之后,雙方開始通過 read 和 write函數來讀寫數據,就像往一個文件流里面寫東西一樣。 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, Mic"); } catch (IOException e) { e.printStackTrace(); }finally { if(out!=null){ out.close(); } if(socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
基于 TCP 實現雙向通信對話功能
TCP 是一個全雙工協(xié)議,數據通信允許數據同時在兩個方向上傳輸,因此全雙工是兩個單工通信方式的結合,它要求發(fā)送設備和接收設備都有獨立的接收和發(fā)送能力。 我們來做一個簡單的實現
Server 端

Client 端
總結
通過一個圖來簡單描述一下 socket 鏈接建立以及通信的模型
理解 TCP 的通信原理及 IO 阻塞
首先,對于 TCP 通信來說,每個 TCP Socket 的內核中都有一個發(fā)送緩沖區(qū)和一個接收緩沖區(qū),TCP 的全雙工的工作模式及 TCP 的滑動窗口就是依賴于這兩個獨立的 Buffer 和該 Buffer的填充狀態(tài)。接收緩沖區(qū)把數據緩存到內核,若應用進程一直沒有調用 Socket 的 read 方法進行讀取,那么該數據會一直被緩存在接收緩沖區(qū)內。不管進程是否讀取 Socket,對端發(fā)來的數據都會經過內核接收并緩存到 Socket 的內核接收緩沖區(qū)。read 所要做的工作,就是把內核接收緩沖區(qū)中的數據復制到應用層用戶的 Buffer 里。進程調用 Socket 的 send 發(fā)送數據的時候,一般情況下是將數據從應用層用戶的 Buffer 里復制到 Socket 的內核發(fā)送緩沖區(qū),然后 send 就會在上層返回。換句話說,send 返回時,數據不一定會被發(fā)送到對端
前面我們提到,Socket 的接收緩沖區(qū)被 TCP 用來緩存網絡上收到的數據,一直保存到應用進程讀走為止。如果應用進程一直沒有讀取,那么 Buffer 滿了以后,出現的情況是:通知對端TCP 協(xié)議中的窗口關閉,保證 TCP 接收緩沖區(qū)不會移除,保證了 TCP 是可靠傳輸的。如果對方無視窗口大小發(fā)出了超過窗口大小的數據,那么接收方會把這些數據丟棄。
滑動窗口協(xié)議
這個過程中涉及到了 TCP 的滑動窗口協(xié)議,滑動窗口(Sliding window)是一種流量控制技術。早期的網絡通信中,通信雙方不會考慮網絡的擁擠情況直接發(fā)送數據。由于大家不知道網絡擁塞狀況,同時發(fā)送數據,導致中間節(jié)點阻塞掉包,誰也發(fā)不了數據,所以就有了滑動窗口機制來解決此問題;發(fā)送和接受方都會維護一個數據幀的序列,這個序列被稱作窗口
發(fā)送窗口
就是發(fā)送端允許連續(xù)發(fā)送的幀的序號表。發(fā)送端可以不等待應答而連續(xù)發(fā)送的最大幀數稱為發(fā)送窗口的尺寸。
接收窗口
接收方允許接收的幀的序號表,凡落在 接收窗口內的幀,接收方都必須處理,落在接收窗口外的幀被丟棄。接收方每次允許接收的幀數稱為接收窗口的尺寸。
在線滑動窗口演示功能 https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html
理解阻塞到底是什么回事
了解了基本通信原理以后,我們再來思考一個問題,在前面的代碼演示中,我們通過socket.accept 去接收一個客戶端請求,accept 是一個阻塞的方法,意味著 TCP 服務器一次只能處理一個客戶端請求,當一個客戶端向一個已經被其他客戶端占用的服務器發(fā)送連接請求時,雖然在連接建立后可以向服務端發(fā)送數據,但是在服務端處理完之前的請求之前,卻不會對新的客戶端做出響應,這種類型的服務器稱為“迭代服務器”。迭代服務器是按照順序處理客戶端請求,也就是服務端必須要處理完前一個請求才能對下一個客戶端的請求進行響應。但是在實際應用中,我們不能接收這樣的處理方式。所以我們需要一種方法可以獨立處理每一個連接,并且他們之間不會相互干擾。而 Java 提供的多線程技術剛好滿足這個需求,這個機制使得服務器能夠方便處理多個客戶端的請求。
一個客戶端對應一個線程
非阻塞模型
上面這種模型雖然優(yōu)化了 IO 的處理方式,但是,不管是線程池還是單個線程,線程本身的處理個數是有限制的,對于操作系統(tǒng)來說,如果線程數太多會造成 CPU 上下文切換的開銷。因此這種方式不能解決根本問題所以在 Java1.4 以后,引入了 NIO(New IO)的功能,我不希望直接來給大家解釋 NIO 的原理,我還是會基于 BIO 到 NIO 的過程來帶著大家思考
阻塞 IO
前面其實已經簡單講過了阻塞 IO 的原理,我想在這里重申一下什么是阻塞 IO 呢? 就是當客戶端的數據從網卡緩沖區(qū)復制到內核緩沖區(qū)之前,服務端會一直阻塞。以socket接口為例,進程空間中調用 recvfrom,進程從調用 recvfrom 開始到它返回的整段時間內都是被阻塞的,因此被成為阻塞 IO 模型
非阻塞 IO
那大家思考一個問題,如果我們希望這臺服務器能夠處理更多的連接,怎么去優(yōu)化呢?我們第一時間想到的應該是如何保證這個阻塞變成非阻塞吧。所以就引入了非阻塞 IO 模型,
非阻塞 IO 模型的原理很簡單,就是進程空間調用 recvfrom,如果這個時候內核緩沖區(qū)沒有數據的話,就直接返回一個 EWOULDBLOCK 錯誤,然后應用程序通過不斷輪詢來檢查這個狀態(tài)狀態(tài),看內核是不是有數據過來。
I/O 復用模型
我們前面講的非阻塞仍然需要進程不斷的輪詢重試。能不能實現當數據可讀了以后給程序一個通知呢?所以這里引入了一個 IO 多路復用模型,I/O 多路復用的本質是通過一種機制(系統(tǒng)內核緩沖 I/O 數據),讓單個進程可以監(jiān)視多個文件描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程序進行相應的讀寫操作
【什么是 fd:在 linux 中,內核把所有的外部設備都當成是一個文件來操作,對一個文件的讀寫會調用內核提供的系統(tǒng)命令,返回一個 fd(文件描述符)。而對于一個 socket 的讀寫也會有相應的文件描述符,成為 socketfd】
常見的 IO 多路復用方式有【select、poll、epoll】,都是 Linux API 提供的 IO 復用方式,那么接下來重點講一下 select、和 epoll 這兩個模型
select:進程可以通過把一個或者多個 fd 傳遞給 select 系統(tǒng)調用,進程會阻塞在 select 操作上,這樣 select 可以幫我們檢測多個 fd 是否處于就緒狀態(tài)。這個模式有二個缺點
1. 由于他能夠同時監(jiān)聽多個文件描述符,假如說有 1000 個,這個時候如果其中一個 fd 處于就緒狀態(tài),那么當前進程需要線性輪詢所有的 fd,也就是監(jiān)聽的 fd 越多,性能開銷越大。
2. 同時,select 在單個進程中能打開的 fd 是有限制的,默認是 1024,對于那些需要支持單機上萬的 TCP 連接來說確實有點少
epoll:linux 還提供了 epoll 的系統(tǒng)調用,epoll 是基于事件驅動方式來代替順序掃描,因此性能相對來說更高,主要原理是,當被監(jiān)聽的 fd 中,有 fd 就緒時,會告知當前進程具體哪一個 fd 就緒,那么當前進程只需要去從指定的 fd 上讀取數據即可另外,epoll 所能支持的 fd 上線是操作系統(tǒng)的最大文件句柄,這個數字要遠遠大于 1024
【由于 epoll 能夠通過事件告知應用進程哪個 fd 是可讀的,所以我們也稱這種 IO 為異步非阻塞 IO,當然它是偽異步的,因為它還需要去把數據從內核同步復制到用戶空間中,真正的異步非阻塞,應該是數據已經完全準備好了,我只需要從用戶空間讀就行】
多路復用的好處
I/O 多路復用可以通過把多個 I/O 的阻塞復用到同一個 select 的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時處理多個客戶端請求。它的最大優(yōu)勢是系統(tǒng)開銷小,并且不需要創(chuàng)建
新的進程或者線程,降低了系統(tǒng)的資源開銷,關于異步 IO 這塊,我就不在這里展開了。后續(xù)在 netty 中還會講到 IO 的內容
一臺機器理論能支持的連接數
這里再補充一點小知識,理論上一臺機器能夠支撐多少個連接,有沒有同學知道?首先,在確定最大連接數之前,大家先跟我來先了解一下系統(tǒng)如何標識一個 tcp 連接。系統(tǒng)用 一 個 四 元 組 來 唯 一 標 識 一 個 TCP 連接: (source_ip, source_port, destination_ip,destination_port)。即(源 IP,源端口,目的 IP,目的端口)四個元素的組合。只要四個元素的組合中有一個元素不一樣,那就可以區(qū)別不同的連接,
比如:
你的 IP 地址是 11.1.2.3, 在 8080 端口監(jiān)聽那么當一個來自 22.4.5.6 ,端口為 5555 的連接到達后,那么建立的這條連接的四元組為 :(11.1.2.3, 8080, 22.4.5.6, 5555)這時,假設上面的那個客戶(22.4.5.6)發(fā)來第二條連接請求,端口為 6666,那么,新連接的四元組為(11.1.2.3, 8080, 22.4.5.6, 5555)那么,你主機的 8080 端口建立了兩條連接;通常來說,服務端是固定一個監(jiān)聽端口,比如 8080,等待客戶端的連接請求。在不考慮地址重用的情況下,及時 server 端有多個 ip,但是本地監(jiān)聽的端口是獨立的。所以對于 tcp 連接的4元組中,如果destination_ip和destination_port不變。那么只有source_ip和source_port是可變的,因此最大的 tcp 連接數應該為 客戶端的 ip 數 乘以 客戶端的端口數。在 IPV4 中,不考慮 ip 分類等因素,最大的 ip 數為 2 的 32 次方 ;客戶端最大的端口數為 2 的 16 次方,也就是 65536. 也就是服務端單機最大的 tcp 連接數約為 2 的 48 次方。
當然,這只是一個理論值,以 linux 服務器為例,實際的連接數還取決于
1. 內存大小(因為每個 TCP 連接都要占用一定的內存)、
2. 文件句柄限制,每一個 tcp 連接都需要占一個文件描述符,一旦這個文件描述符使用完了,新來的連接會返回一個“Can’t open so many files”的異常。如果大家知道對于操作系統(tǒng)最
大可以打開的文件數限制,就知道怎么去調整這個限制
a) 可以執(zhí)行【ulimit -n】得到當前一個進程最大能打開 1024 個文件,所以你要采用此默認配置最多也就可以并發(fā)上千個 TCP 連接。
b) 可以通過【vim /etc/security/limits.conf】去修改系統(tǒng)最大文件打開數的限制
* soft nofile 2048
* hard nofile 2048
* 表示修改所有用戶限制、soft/hard 表示軟限制還是硬限制,2048 表示修改以后的值
c) 可以通過【cat /proc/sys/fs/file-max】查看 linux 系統(tǒng)級最大打開文件數限制,表示當前這個服務器最多能同時打開多少個文件當然,這塊還有其他很多的優(yōu)化的點,這里不是這節(jié)課的目標
3. 帶寬資源的限制總結
這節(jié)課基于 http 請求通信到 tcp,再到 tcp 通信的原理以及 IO 阻塞這塊了解了通信的一些基本知識,如果對于這塊有感興趣的同學,可以去找再去找一些資料研究研究,實際上我們只需要掌握到我講解的程度就行。

浙公網安備 33010602011771號