主頁 > 後端開發 > 詳細剖析分布式微服務架構下網路通信的底層實作原理(圖解)

詳細剖析分布式微服務架構下網路通信的底層實作原理(圖解)

2021-11-09 06:12:33 後端開發

在分布式架構中,網路通信是底層基礎,沒有網路,也就沒有所謂的分布式架構,只有通過網路才能使得一大片機器互相協作,共同完成一件事情,

同樣,在大規模的系統架構中,應用吞吐量上不去、網路存在通信延遲、我們首先考慮的都是網路問題,因此網路的重要性不言而喻,

作為現代化應用型程式員,要開發一個網路通信的應用,是非常簡單的,不僅僅有成熟的api,還有非常方便的通信框架,

可能大家已經忘記了網路通信的重要性,本篇文章會詳細分析網路通信的底層原理!!

1.1 理解通信的本質

如圖1-1所示,當我們通過瀏覽器訪問一個網址時,一段時間后該網址會渲染出訪問的內容,這個程序是怎么實作的呢?

image-20210821151126835

圖1-1

我想站在今天,在做的同學都知道,它是基于http協議來實作資料通信的,這里有兩個字很重要,就是“協議”,

兩個計算機之間要實作資料通信,必須遵循同一種協議,否則,就像一個中國人和一個外國人交流時,一個講英語另一個講解中文,肯定是無法正常交流,在計算機中,協議非常常見,

1.1.1 協議的組成

我們寫的Java代碼,計算機能夠理解并且執行,原因是人和計算機之間遵循了同一種語言,那就是Java,如圖1-2所示,.java檔案最終編譯成.class檔案這個程序,也同樣涉及到協議,

image-20210821151817974

圖1-2 java編譯程序

所以,在計算機中,協議是指大家需要共同遵循的規則,只有實作統一規則之后,才能實作不同節點之間的資料通信,從而讓計算機的應用更加強大,

組成一個協議,需要具備三個要素:

  • 語法,就是這一段內容要符合一定的規則和格式,例如,括號要成對,結束要使用分號等,
  • 語意,就是這一段內容要代表某種意義,例如數字減去數字是有意義的,數字減去文本一般來說就沒有意義,
  • 時序,就是先干啥,后干啥,例如,可以先加上某個數值,然后再減去某個數值,

1.1.2 http協議

理解了協議的作用,那協議是長什么樣的呢?

那么再來看圖1-3的場景,人們通過瀏覽器訪問網站,用到了http協議,

image-20210821151126835

圖1-3 http協議

http協議包含包含幾個部分:

  • http請求組成
    • 狀態行
    • 請求頭
    • 訊息主體
  • http回應組成
    • 狀態行
    • 回應頭
    • 回應正文

Http回應報文如圖1-4所示,那么這個協議的三要素分別是:

  • 語法: http協議的訊息體由狀態、頭部、內容組成,
  • 語意: 比如狀態,200表示成功,404表示請求路徑不存在等,通信雙方必須遵循該語意,
  • 時序: 組成訊息體的三部分的排列順序,必須要有request,才會產生response,

而瀏覽器按照http協議做好了相關的處理后,才能讓大家通過網址訪問網路上的各種資訊,

image-20210821155309605

圖1-4

1.1.3 常用的網路協議

DNS協議、Http協議、SSH協議、TCP協議、FTP協議等,這些都是大家比較常用的協議型別,無論哪種協議,本質上仍然是由協議的三要素組成,只是應用場景不同,

DNS、HTTP、HTTPS 所在的層我們稱為應用層,經過應用層封裝后,瀏覽器會將應用層的包交給下一層去完成,通過 socket 編程來實作,下一層是傳輸層,傳輸層有兩種協議,一種是無連接的協議 UDP,一種是面向連接的協議 TCP,對于通信可靠性要求的場景來說,往往使用 TCP 協議,所謂的面向連接就是,TCP 會保證這個包能夠到達目的地,如果不能到達,就會重新發送,直至到達,

1.3 TCP/IP通信原理分析

一次網路通信到底是怎么完成的呢?

涉及到網路通信,那我們一定會提到一個網路模型的概念,如圖1-5所示,表示TCP/IP的四層概念模型和OSI七層網路模型,它是一種概念模型,由國際標準化組織提出來的,試圖讓全世界范圍內的計算機能基于該網路標準實作互聯,

image-20210821170017901

圖1-5

網路模型為什么要分層呢?其實從我們現在的業務分層架構中就不難發現,任何系統一旦變得復雜,就都會采用分層設計,它的主要好處是

  • 實作高內聚低耦合
  • 每一層有自己單一的職責
  • 提高可復用性和降低維護成本

1.2.1 http通信程序的發送資料包

由于我們的課程并不是專門來講網路,所以只是提及一下網路分層模型,為了讓大家更簡單的理解網路分層模型的作業原理,我們仍然以一次網路通信的資料包傳輸為例進行分析,如圖1-6所示,

image-20210821175033443

圖1-6

圖1-6的作業流程描述如下:

  • 假設我們要登錄某一個網站,此時基于Http協議會構建一個http協議報文,這個報文中按照http協議的規范組裝,其中包括要傳輸的用戶名和密碼,這個是屬于應用層協議,

  • 經過應用層封裝后,瀏覽器會把應用層的包交給TCP/IP四層模型中的下一層,也就是傳輸層來完成,傳輸層有兩種協議:

    • TCP協議,可靠的通信協議,該協議會確保資料包能達到目的地
    • UDP協議,不可靠通信協議,可能會存在資料丟失

    在http通信中使用了TCP協議,TCP協議會有兩個埠,一個是瀏覽器監聽的埠,一個是目標服務器行程的埠,作業系統會根據埠來判斷這個資料包應該分發給那個行程,

  • 傳輸層封裝完成后,該資料包會技術交給網路層來處理,網路層協議是IP協議,IP協議中會包含源IP地址(也就是客戶端及其的IP)和目標服務器的IP地址,

  • 作業系統知道了目標IP地址后,就開始根據這個IP來尋找目標機器,而目標服務器一定是部署在不同的地方,這種跨網路節點的訪問,需要經過網關(所謂網關就是一個網路到另外一個網路的關口),

    所以資料包首先需要先通過自己當前所在網路的網關出去,然后訪問到目標服務器,但是在資料包傳輸到目標服務器之前,需要再組裝MAC頭資訊,

    Mac頭包含本地的Mac地址和目標服務器的Mac地址,這個MAC地址怎么獲得的呢?

    • 獲取本機MAC地址的方法是,作業系統會發送一個廣播訊息詢問網關地址(192.168.1.1)是誰?收到該廣播訊息的網關會回應一個MAC地址,這個廣播訊息是基于ARP協議實作的(這個協議簡單來說就是已知目標機器的ip,需要獲得目標機器的mac地址,(發送一個廣播訊息,這個ip是誰的,請來認領,認領ip的機器會發送一個mac地址的回應)),

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

    • 獲取遠程機器的MAC地址的方法也同樣是基于ARP協議實作的,

完成MAC地址組裝后,一個完整的資料包就構成了,這個時候會把這個資料包給到網卡,網卡再把這個資料包發出去,由于這個資料包中包含MAC地址,因此它能夠到達網關進行傳輸,網關收到包之后,會根據路由資訊,判斷下一步應該怎么走,網關往往是一個路由器,到某個 IP 地址應該怎么走,這個叫作路由表,

1.2.2 http通信程序中的接收資料包

當資料包發送到網關后,會根據網關的路由資訊判斷該資料包要傳輸到那個網段上,資料從客戶端發送到目標服務器,可能會經過多個網關,所以資料包根據網關路由進入到下一個網關后,繼續根據下一個網關的MAC地址尋找下下一個網關,直到到達目標網路服務器上,

這個時候服務器收到包之后,最后一個網關知道這個網路包就是要去當前局域網的,于是拿著目標IP通過ARP協議大喊一聲這是誰? 目標服務器就會給網關回復一個MAC地址, 然后網路包在最后那個網關修改目標的MAC地址,通過這個MAC地址,網路包找到了目標服務器,

當目標服務器和MAC地址對上后,開始取出MAC頭資訊,接著把資料包發送給作業系統的網路層,網路層會取出IP頭資訊,IP頭里面會寫上一層封裝的是TCP協議,于是交給傳輸層來處理,實作程序如圖1-7所示,

在這一層中,對于收到的每個資料包都會有一個回復,表示服務器端已經收到了該資料包,如果過一段時間客戶端沒有收到該確認包,發送端的 TCP 層會重新發送這個包,還是上面的程序,直到最終收到回復,

這個重試是TCP協議層來實作的,不需要我們應用來主動發起,

image-20210822152538600

圖1-7

為什么有了MAC層還要走IP層呢?

之前我們提到,mac地址是唯一的,那理論上,在任何兩個設備之間,我應該都可以通過mac地址發送資料,為什么還需要ip地址?

mac地址就好像個人的身份證號,人的身份證號和人戶口所在的城市,出生的日期有關,但是和人所在的位置沒有關系,人是會移動的,知道一個人的身份證號,并不能找到它這個人,mac地址類似,它是和設備的生產者,批次,日期之類的關聯起來,知道一個設備的mac,并不能在網路中將資料發送給它,除非它和發送方的在同一個網路內,

所以要實作機器之間的通信,我們還需要有ip地址的概念,ip地址表達的是當前機器在網路中的位置,類似于城市名+道路號+門牌號的概念,通過ip層的尋址,我們能知道按何種路徑在全世界任意兩臺Internet上的的機器間傳輸資料,

1.4 詳解TCP可靠性通信特性

我們知道,TCP協議是屬于可靠性通信協議,它能夠確保資料包不被丟失,首先我們先了解一下TCP的三次握手和四次揮手,

1.4.1 TCP的三次握手

兩個節點需要進行資料通信,首先得先建立連接,而在建立連接時,TCP采用了三次握手來實作連接建立,如圖1-8所示,

image-20210822161148923

圖1-8

第一次握手(SYN=1, seq=x)

客戶端發送一個 TCP的 SYN 標志位置1的包,指明客戶端打算連接的服務器的埠,以及初始序號 X,保存在包頭的序列號(Sequence Number)欄位里,發送完畢后,客戶端進入 SYN_SEND 狀態,

第二次握手(SYN=1, ACK=1, seq=y, ACK num=x+1):

服務器發回確認包(ACK)應答,即 SYN 標志位和 ACK 標志位均為1,服務器端選擇自己 ISN 序列號,放到Seq 域里,同時將確認序號(Acknowledgement Number)設定為客戶的 ISN 加1,即X+1, 發送完畢后,服務器端進入 SYN_RCVD 狀態,

第三次握手(ACK=1,ACK num=y+1)

客戶端再次發送確認包(ACK),SYN標志位為0,ACK標志位為1,并且把服務器發來 ACK的序號欄位+1,放在確定欄位中發送給對方,并且在資料段放寫ISN發完畢后,客戶端進入 ESTABLISHED 狀態,當服務器端接收到這個包時,也進入 ESTABLISHED 狀態,TCP握手結束,

1.4.2 TCP為什么是三次握手?

TCP是全雙工,如果沒有第三次的握手,服務端不能確認客戶端是否ready,不知道什么時候可以往客戶端發資料包,三次的握手剛好兩邊都互相確認對方已經ready,

我們假設網路的不可靠性,

A發起一個連接,當發起一個請求沒有得到反饋的時候,會有很多可能性,比如請求包丟失,或者超時,或者B沒有回應

由于A不能確認結果,于是再發,當有一個請求包到了B之后,A并不知道這個資料包已經到了B,所以可能還會重試,

所以B收到請求之后,知道了A的存在并且要和我建立連接,這個時候B會發送ack給到A,告訴A我收到了請求包,

對于B來說,這個應答包也是一個網路通信,我怎么知道能不能到達A呢?所以這個時候B不能很主觀的認為連接已經建立好了,還需要等到A再次發送應答包來確認,

1.4.3 TCP的四次揮手

如圖1-9所示,TCP的連接斷開,會通過所謂的四次揮手完成,

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

image-20210822162756038

圖1-9

上述互動程序如下:

  • 斷開的時候,我們可以看到,當A客戶端說說“我要斷開連接”,就進入 FIN_WAIT_1 的狀態,

  • B 服務端收到“我要斷開連接”的訊息后,發送"知道了"給到A客戶端,就進入 CLOSE_WAIT 的狀態,

  • A 收到“B 說知道了”,就進入 FIN_WAIT_2 的狀態,如果這個時候 B 服務器掛掉了,則 A 將永遠在這個狀態,TCP 協議里面并沒有對這個狀態的處理,但是 Linux 有,可以調整 tcp_fin_timeout 這個引數,設定一個超時時間,

  • 如果 B 服務器正常,則發送了“B 要關閉連接”的請求到達 A 時,A 發送“知道 B 也要關閉連接”的 ACK 后,從 FIN_WAIT_2 狀態結束,

  • 按說這個時候 A 可以退出了,但是最后的這個 ACK 萬一 B 收不到呢?則 B 會重新發一個“B 要關閉連接”,這個時候 A 已經跑路了的話,B 就再也收不到 ACK 了,因而 TCP 協議要求 A 最后等待一段時間 TIME_WAIT,這個時間要足夠長,長到如果 B 沒收到 ACK 的話,“B 說不玩了”會重發的,A 會重新發一個 ACK 并且足夠時間到達 B,

這個等待實作是2MSL,MSL 是 Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網路上存在的最長時間,超過這個時間報文將被丟棄(此時A直接進入CLOSE狀態),協議規定 MSL 為 2 分鐘,實際應用中常用的是 30 秒,1 分鐘和 2 分鐘等,

第一次揮手(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 狀態,

【問題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報文,

1.4.4 TCP協議的報文傳輸

連接建立好之后,就開始進行資料包的傳輸了,那TCP作為一個可靠的通信協議,如何保證訊息傳輸的可靠性呢?

TCP采用了訊息確認的方式來保證資料報文傳輸的安全性,也就是說客戶端發送了資料包到服務端后,服務端會回傳一個確認訊息給到客戶端,如果客戶端沒有收到確認包,則會重新再發送,

為了保證順序性,每一個包都有一個 ID,在建立連接的時候,會商定起始的 ID 是什么,然后按照 ID 一個個發送,為了保證不丟包,對于發送的包都要進行應答,但是這個應答也不是一個一個來的,而是會應答某個之前的 ID,表示都收到了,這種模式稱為累計確認或者累計應答(cumulative acknowledgment)

如圖1-10所示,為了記錄所有發送的包和接收的包,TCP協議在發送端和接收端分別拿會有發送緩沖區和接識訓沖區,TCP的全雙工的作業模式及TCP的滑動視窗就是依賴于這兩個獨立的Buffer和該Buffer的填充狀態,

接識訓沖區把資料快取到內核,若應用行程一直沒有呼叫Socket的read方法進行讀取,那么該資料會一直被快取在接識訓沖區內,不管行程是否讀取Socket,對端發來的資料都會經過內核接收并快取到Socket的內核接識訓沖區,

read所要做的作業,就是把內核接識訓沖區中的資料復制到應用層用戶的Buffer里,行程呼叫Socket的send發送資料的時候,一般情況下是將資料從應用層用戶的Buffer里復制到Socket的內核發送緩沖區,然后send就會在上層回傳,換句話說,send回傳時,資料不一定會被發送到對端,

image-20210822173920975

圖1-10

發送端/接收端的緩沖區中是按照包的 ID 一個個排列,根據處理的情況分成四個部分,

  • 第一部分:發送了并且已經確認的,
  • 第二部分:發送了并且尚未確認的,需要等待確認后,才能移除,
  • 第三部分:沒有發送,但是已經等待發送的,
  • 第四部分:沒有發送,并且暫時還不會發送的,

這里的第三部分和第四部分之所以做一個區分,其實是因為TCP采用做了流量控制,這里采用了滑動視窗的方式來實作流量整形,避免出現資料擁堵的情況,

image-20210822173129991

圖1-11

為了更好的理解資料包的通信程序,我們通過下面這個網址來演示一下

https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html

1.4.5 滑動視窗協議

上述地址中影片演示的部分,其實就是資料包發送和確認機制,同時還涉及到互動視窗協議,

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

發送視窗

就是發送端允許連續發送的幀的序號表,

發送端可以不等待應答而連續發送的最大幀數稱為發送視窗的尺寸,

接收視窗

接收方允許接收的幀的序號表,凡落在 接收視窗內的幀,接收方都必須處理,落在接收視窗外的幀被丟棄,

接收方每次允許接收的幀數稱為接收視窗的尺寸,

1.5 理解阻塞通信的本質

理解了TCP通信的原理后,在Java中我們會采用Socket套接字來實作網路通信,下面這段代碼演示了Socket通信的案例,

public class ServerSocketExample {

    public static void main(String[] args) throws IOException {
        final int DEFAULT_PORT = 8080;
        ServerSocket serverSocket = null;
        serverSocket = new ServerSocket(DEFAULT_PORT);
        System.out.println("啟動服務,監聽埠:" + DEFAULT_PORT);
        while (true) {
            Socket socket = serverSocket.accept();
            System.out.println("客戶端:" + socket.getPort() + "已連接");
            new Thread(new Runnable() {
                Socket socket;
                public Runnable setSocket(Socket s){
                    this.socket=s;
                    return this;
                }
                @Override
                public void run() {
                    try {
                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        String clientStr = null; //讀取一行資訊
                        clientStr = bufferedReader.readLine();
                        System.out.println("客戶端發了一段訊息:" + clientStr);
                        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                        bufferedWriter.write("我已經收到你的訊息了");
                        bufferedWriter.flush(); //清慷訓沖區觸發訊息發送
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.setSocket(socket)).start();

        }
    }
}

在我們講Redis的專題中詳細講到過,上述通信是BIO模型,也就是阻塞通信模型,阻塞主要體現的點是

  • accept,阻塞等待客戶端連接
  • io阻塞,阻塞等待客戶端的資料傳輸,

相信大家和我一樣有一些以后,這個阻塞和喚醒到底是怎么回事,下面我們簡單來了解一下,

1.5.1 阻塞操作的本質

阻塞是指行程在等待某個事件發生之前的等待狀態,它是屬于作業系統層面的調度,我們通過下面操作來追蹤Java程式中有多少程式,每一個執行緒對內核產生了哪些操作,

strace,Linux作業系統中的指令

  1. 把ServerSocketExample.java,去掉package匯入頭,拷貝到linux服務器的 /data/app目錄下,

  2. 使用javac ServerSocketExample.java進行編譯,得到.class檔案

  3. 使用下面這個命令來追蹤(打開一個新視窗)

    按照strace官網的描述, strace是一個可用于診斷、除錯和教學的Linux用戶空間跟蹤器,我們用它來監控用戶空間行程和內核的互動,比如系統呼叫、信號傳遞、行程狀態變更等,

    strace -ff -o out java ServerSocketExample
    
    • -f 跟蹤目標行程,以及目標行程創建的所有子行程
    • -o 把strace的輸出單獨寫到指定的檔案
  4. 上述指令執行完成后,會在/data/app目錄下得到很多out.*的檔案,每個檔案代表一個執行緒,因為Java本身是多執行緒的,

    [root@localhost app]# ll
    total 748
    -rw-r--r--. 1 root root  14808 Aug 23 12:51 out.33320 //最小的表示主執行緒
    -rw-r--r--. 1 root root 186893 Aug 23 12:51 out.33321
    -rw-r--r--. 1 root root    961 Aug 23 12:51 out.33322
    -rw-r--r--. 1 root root    917 Aug 23 12:51 out.33323
    -rw-r--r--. 1 root root    833 Aug 23 12:51 out.33324
    -rw-r--r--. 1 root root    819 Aug 23 12:51 out.33325
    -rw-r--r--. 1 root root  23627 Aug 23 12:53 out.33326
    -rw-r--r--. 1 root root   1326 Aug 23 12:51 out.33327
    -rw-r--r--. 1 root root   1144 Aug 23 12:51 out.33328
    -rw-r--r--. 1 root root   1270 Aug 23 12:51 out.33329
    -rw-r--r--. 1 root root   8136 Aug 23 12:53 out.33330
    -rw-r--r--. 1 root root   8158 Aug 23 12:53 out.33331
    -rw-r--r--. 1 root root   6966 Aug 23 12:53 out.33332
    -rw-r--r--. 1 root root   1040 Aug 23 12:51 out.33333
    -rw-r--r--. 1 root root 445489 Aug 23 12:53 out.33334
    
  5. 打開out.33321這個檔案(主執行緒后面的一個檔案),shift+g到該檔案的尾部,可以看到如下內容,

    下面這些方法,都是屬于系統呼叫,也就是呼叫作業系統提供的內核指令觸發相關的操作,

    # 創建socket fd 
    socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5 
    ....
    # 系結8888埠
    bind(5, {sa_family=AF_INET6, sin6_port=htons(8888), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
    # 創建一個socket并監聽申請的連接, 5表示sockfd,50表示等待佇列的最大長度
    listen(5, 50)                           = 0
    mprotect(0x7f21d00df000, 4096, PROT_READ|PROT_WRITE) = 0
    write(1, "\345\220\257\345\212\250\346\234\215\345\212\241\357\274\214\347\233\221\345\220\254\347\253\257\345\217\243\357\274\23288"..., 34) = 34
    write(1, "\n", 1)                       = 1
    lseek(3, 58916778, SEEK_SET)            = 58916778
    read(3, "PK\3\4\n\0\0\10\0\0U\23\213O\336\274\205\24X8\0\0X8\0\0\25\0\0\0", 30) = 30
    lseek(3, 58916829, SEEK_SET)            = 58916829
    read(3, "\312\376\272\276\0\0\0004\1\367\n\0\6\1\37\t\0\237\1 \t\0\237\1!\t\0\237\1\"\t\0"..., 14424) = 14424
    # poll, 把當前的檔案指標掛到等待佇列,檔案指標指的是fd=5,簡單來說就是讓當前行程阻塞,直到有事件觸發喚醒
    * events: 表示請求事件,POLLIN(普通或優先級帶資料可讀)、POLLERR,發生錯誤,
    poll([{fd=5, events=POLLIN|POLLERR}], 1, -1
    

從這個代碼中可以看到,Socket的accept方法最終是呼叫系統的poll函式來實作執行緒阻塞的,

通過在linux服務器輸入 man 2 poll

man: 幫助手冊

2: 表示系統呼叫相關的函式

DESCRIPTION
       poll()  performs  a  similar  task  to  select(2): it waits for one of a set of file
       descriptors to become ready to perform I/O.

poll類似于select函式,它可以等待一組檔案描述符中的IO就緒事件

  1. 通過下面命令訪問socket server,

    telnet 192.168.221.128 8888
    

    這個時候通過tail -f out.33321這個檔案,發現被阻塞的poll()方法,被POLLIN事件喚醒了,表示監聽到了一次連接,

    poll([{fd=5, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=5, revents=POLLIN}])
    accept(5, {sa_family=AF_INET6, sin6_port=htons(53778), inet_pton(AF_INET6, "::ffff:192.168.221.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
    

1.5.2 阻塞被喚醒的程序

如圖1-12所示,網路資料包通過網線傳輸到目標服務器的網卡,再通過2所示的硬體電路傳輸,最終把資料寫入到記憶體中的某個地址上,接著網卡通過中斷信號通知CPU有資料到達,作業系統就知道當前有新的資料包傳遞過來,于是CPU開始執行中斷程式,中斷程式的主要邏輯是

  • 先把網卡接收到的資料寫入到對應的Socket接識訓沖區中
  • 再喚醒被阻塞在poll()方法上的執行緒

img

圖1-12

1.5.3 阻塞的整體原理分析

作業系統為了支持多任務處理,所以實作了行程調度功能,運行中的行程表示獲得了CPU的使用權,當行程(執行緒)因為某些操作導致阻塞時,就會釋放CPU使用權,使得作業系統能夠多任務的執行,

當多個行程是運行狀態等待CPU調度時,這些行程會保存到一個可運行佇列中,如圖1-13所示,

image-20210823143314215

圖1-13
當行程A執行創建Socket陳述句時,在Linux作業系統中會創建一個由檔案系統管理的Socket物件,這個Socket物件包含發送緩沖區、接識訓沖區、等待佇列等,其中等待佇列是非常重要的結構,它指向所有需要等待當前Socket事件的行程,如圖1-14所示,

當行程A呼叫poll()方法阻塞時,作業系統會把當前行程A從作業佇列移動到Socket的等待佇列中(將行程A的指標指向等待佇列,后續需要進行喚醒),此時A被阻塞,CPU繼續執行下一個行程,

image-20210823145055784

圖1-14

當Socket收到資料時,等待該Socket FD的行程會收到被喚醒,如圖1-15所示,計算機通過網卡接收到客戶端傳過來的資料,網卡會把這個資料寫入到記憶體,然后再通過中斷信號通知CPU有資料到達,于是CPU開始執行中斷程式,

當發生了中斷,就意味著需要作業系統的介入,開展管理作業,由于作業系統的管理作業(如行程切換、分配IO設備)需要使用特權指令,因此CPU要從用戶態轉換為核心態,中斷就可以使CPU從用戶態轉換為核心態,使作業系統獲得計算機的控制權,因此,有了中斷,才能實作多道程式并發執行,

此處的中斷程式主要有兩項功能,先將網路資料寫入到對應 Socket 的接識訓沖區里面(步驟 ④),再喚醒行程 A(步驟 ⑤),重新將行程 A 放入作業佇列中,

image-20210823150147501

圖1-15

1.5 Linux中的select/poll模型本質

前面在1.4節中講的其實是Recv()方法,它只能監視單個Socket,而在實際應用中,這種單Socket監聽很明顯會影響到客戶端連接數,所以我們需要尋找一種能夠同時監聽多個Socket的方法,而select/poll就是在這個背景下產生的,其中poll方法在前面的案例中就講過,默認情況下使用poll模型,

先來了解一下select模型,由于在前面的分析中我們知道Recv()只能實作對單個socket的監聽,當客戶端連接數較多的時候,會導致吞吐量非常低,所以我們想,能不能實作同時監聽多個socket,只要任何一個socket連接存在IO就緒事件,就觸發行程的喚醒,

如圖1-16所示,假設程式同時監聽socket1和socket2這兩個socket連接,那么當應用程式呼叫select方法后,作業系統會把行程A分別指向這連個個socket的等待佇列中,當任何一個Socket收到資料后,中斷程式會喚醒對應的行程,

當行程 A 被喚醒后,它知道至少有一個 Socket 接收了資料,程式只需遍歷一遍 Socket 串列,就可以得到就緒的 Socket,

image-20210823153708311

圖1-16

select模式有二個問題,

  • 就是每次呼叫select都需要將行程加入到所有監視器socket的等待佇列,每次喚醒都需要從等待佇列中移除,這里涉及到兩次遍歷,有一定的性能開銷,
  • 行程被喚醒后,并不知道哪些socket收到了資料,所以還需要遍歷一次所有的socket,得到就緒的socket串列

由于這兩個問題產生的性能影響,所以select默認規定只能監視1024個socket,雖然可以通過修改監視的檔案描述符數量,但是這樣會降低效率,而poll模式和select基本是一樣,最大的區別是poll沒有最大檔案描述符限制,

1.6 Linux中的epoll模型

有沒有更加高效的方法,能夠減少遍歷也能達到同時監聽多個fd的目的呢?epoll模型就可以解決這個問題,

epoll 其實是event poll的組合,它和select最大的區別在于,epoll會把哪個socket發生了什么樣的IO事件通知給應用程式,所以epoll實際上就是事件驅動,具體原理如圖1-17所示,

在epoll中提供了三個方法分別是epoll_create、epoll_ctl、epoll_wait,具體執行流程如下

  • 首先呼叫epoll_create方法,在內核創建一個eventpoll物件,這個物件會維護一個epitem集合,它是一個紅黑樹結構,這個集合簡單理解成fd集合,
  • 接著呼叫epoll_ctl函式將當前fd封裝成epitem加入到eventpoll物件中,并給這個epitem加入一個回呼函式注冊到內核,當這個fd收到網路IO事件時,會把該fd對應的epitem加入到eventpoll中的就緒串列rdlist(雙向鏈表)中,同時再喚醒被阻塞的行程A,
  • 行程A繼續呼叫epoll_wait方法,直接讀取epoll中就緒佇列rdlist中的epitem,如果rdlist佇列為空,則阻塞等待或者等待超時,

從epoll的原理中可以得知,由于rdlist的存在,使得行程A被喚醒后知道哪些Socket(fd)發生了IO事件,從而在不需要遍歷的情況下獲取所有就緒的socket連接,

image-20210823212943850

圖1-17

著作權宣告:本博客所有文章除特別宣告外,均采用 CC BY-NC-SA 4.0 許可協議,轉載請注明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力,歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術干貨!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/353111.html

標籤:Java

上一篇:Spring自動裝配

下一篇:音視頻處理、影像處理、影像識別和字符識別全能庫JavaCV完整教程(包含完整JavaCV入門、JavaCV實戰、ffmpeg、opencv和tesserac教程)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more