0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內(nèi)不再提示

探究Redis網(wǎng)絡模型究竟有多強大(上)

jf_78858299 ? 來源:蟬沐風的碼場 ? 作者:蟬沐風 ? 2023-03-03 09:46 ? 次閱讀

如果面試官問我:Redis為什么這么快?

我肯定會說:因為Redis是內(nèi)存數(shù)據(jù)庫!如果不是直接把數(shù)據(jù)放在內(nèi)存里,甭管怎么優(yōu)化數(shù)據(jù)結(jié)構(gòu)、設計怎樣的網(wǎng)絡I/O模型,都不可能達到如今這般的執(zhí)行效率。

但是這么回答多半會讓我直接回去等通知了。。。因為面試官想聽到的就是數(shù)據(jù)結(jié)構(gòu)和網(wǎng)絡模型方面的回答,雖然這兩者只是在內(nèi)存基礎上的錦上添花。

說這些并非為了強調(diào)網(wǎng)絡模型并不重要,恰恰相反,它是Redis實現(xiàn)高吞吐量的重要底層支撐,是“高性能”的重要原因,卻不是“快”的直接理由。

本文將從BIO開始介紹,經(jīng)過NIO、多路復用,最終說回Redis的Reactor模型,力求詳盡。本文與其他文章的不同點主要在于:

1、不會介紹同步阻塞I/O、同步非阻塞I/O、異步阻塞I/O、異步非阻塞I/O等概念,這些術語只是對底層原理的一些概念總結(jié)而已,我覺得沒有用。底層原理搞懂了,這些概念根本不重要,我希望讀完本文之后,各位能夠不再糾結(jié)這些概念。

2、不會只拿生活中例子來說明問題。之前看過特別多的文章,這些文章舉的“燒水”、“取快遞”的例子真的是深入淺出,但是看懂這些例子會讓我們有一種我們真的懂了的錯覺。尤其對于網(wǎng)絡I/O模型而言,很難找到生活中非常貼切的例子,這種例子不過是已經(jīng)懂了的人高屋建瓴,對外輸出的一種形式,但是對于一知半解的讀者而言卻猶如鈍刀殺人。

牛皮已經(jīng)吹出去了,正文開始。

1. 一次I/O到底經(jīng)歷了什么

我們都知道,網(wǎng)絡I/O是通過Socket實現(xiàn)的,在說明網(wǎng)絡I/O之前,我們先來回顧(了解)一下本地I/O的流程。

舉一個非常簡單的例子,下面的代碼實現(xiàn)了文件的拷貝,將file1.txt的數(shù)據(jù)拷貝到file2.txt中:

public static void main(String[] args) throws Exception {
  
    FileInputStream in = new FileInputStream("/tmp/file1.txt");
    FileOutputStream out = new FileOutputStream("/tmp/file2.txt");

    byte[] buf = new byte[in.available()];
    in.read(buf);
    out.write(buf);
}

這個I/O操作在底層到底經(jīng)歷了什么呢?下圖給出了說明:

圖片

本地I/O示意圖

大致可以概括為如下幾個過程:

  • in.read(buf)執(zhí)行時,程序向內(nèi)核發(fā)起 read()系統(tǒng)調(diào)用;
  • 操作系統(tǒng)發(fā)生上下文切換,由用戶態(tài)(User mode)切換到內(nèi)核態(tài)(Kernel mode),把數(shù)據(jù)讀取到內(nèi)核緩沖區(qū) (buffer)中;
  • 內(nèi)核把數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,同時由內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài);
  • 繼續(xù)執(zhí)行 out.write(buf)
  • 再次發(fā)生上下文切換,將數(shù)據(jù)從用戶空間buffer拷貝到內(nèi)核空間buffer中,由內(nèi)核把數(shù)據(jù)寫入文件。

之所以先拿本地I/O舉個例子,是因為我想說明I/O模型并非僅僅針對網(wǎng)絡IO(雖然網(wǎng)絡I/O最常被我們拿來舉例),本地I/O同樣受到I/O模型的約束。比如在這個例子中,本地I/O用的就是典型的BIO,至于什么是BIO,稍安勿躁,接著往下看。

除此之外,通過本地I/O,我還想向各位說明下面幾件事情:

  1. 我們編寫的程序本身并不能對文件進行讀寫操作,這個步驟必須依賴于操作系統(tǒng),換個詞兒就是「內(nèi)核」;
  2. 一個看似簡單的I/O操作卻在底層引發(fā)了多次的用戶空間和內(nèi)核空間的切換,并且數(shù)據(jù)在內(nèi)核空間和用戶空間之間拷貝來拷貝去。

不同于本地I/O是從本地的文件中讀取數(shù)據(jù),網(wǎng)絡I/O是通過網(wǎng)卡讀取網(wǎng)絡中的數(shù)據(jù),網(wǎng)絡I/O需要借助Socket來完成,所以接下來我們重新認識一下Socket。

2. 什么是Socket

這部分在一定程度上是我的強迫癥作祟,我關于文章對知識點講解的完備性上對自己近乎苛刻。我覺得把Socket講明白對接下來的講解是一件很重要的事情,看過我之前的文章的讀者或許能意識到,我盡量避免把前置知識直接以鏈接的形式展示出來,我認為會割裂整篇文章的閱讀體驗。

不割裂的結(jié)果就是文章可能顯得很啰嗦,好像一件事情非得從盤古開天辟地開始講起。因此,如果各位覺得對這個知識點有足夠的把握,就直接略過好了~

我們所做的任何需要和遠程設備進行交互的操作,并非是操作軟件本身進行的數(shù)據(jù)通信。舉個例子就是我們用瀏覽器刷B站視頻的時候,并非是瀏覽器自身向B站請求視頻數(shù)據(jù)的,而是必須委托操作系統(tǒng)內(nèi)核中的協(xié)議棧。

圖片

網(wǎng)絡I/O

而Socket庫就是操作系統(tǒng)提供給我們的,用于調(diào)用協(xié)議棧網(wǎng)絡功能的一堆程序組件的集合,也就是我們平時聽過的操作系統(tǒng)庫函數(shù),Socket庫和協(xié)議棧的關系如下圖所示。

圖片

Socket庫和協(xié)議棧的關系

用戶進程向操作系統(tǒng)內(nèi)核的協(xié)議棧發(fā)出委托時,需要按照指定的順序來調(diào)用 Socket 庫中的程序組件。

本文的所有案例都以TCP協(xié)議為例進行講解。

大家可以把數(shù)據(jù)收發(fā)想象成在兩臺計算機之間創(chuàng)建了一條數(shù)據(jù)通道,計算機通過這條通道進行數(shù)據(jù)收發(fā)的雙向操作,當然,這條通道是邏輯上的,并非實際存在。

圖片

TCP連接有邏輯通道

數(shù)據(jù)通過管道流動這個比較好理解,但是問題在于這條管道雖然只是邏輯上存在,但是這個“邏輯”也不是光用腦袋想想就會出現(xiàn)的。就好比我們手機打電話,你總得先把號碼撥出去呀。

對應到網(wǎng)絡I/O中,就意味著雙方必須創(chuàng)建各自的數(shù)據(jù)出入口,然后將兩個數(shù)據(jù)出入口像連接水管一樣接通,這個數(shù)據(jù)出入口就是上圖中的套接字,就是大名鼎鼎的socket。

客戶端和服務端之間的通信可以被概括為如下4個步驟:

  1. 服務端創(chuàng)建socket,等待客戶端連接(創(chuàng)建socket階段);
  2. 客戶端創(chuàng)建socket,連接到服務端(連接階段);
  3. 收發(fā)數(shù)據(jù)(通信階段);
  4. 斷開管道并刪除socket(斷開連接)。

每一步都是通過特定語言的API調(diào)用Socket庫,Socket庫委托協(xié)議棧進行操作的。socket就是調(diào)用Socket庫中程序組件之后的產(chǎn)成品,比如Java中的ServerSocket,本質(zhì)上還是調(diào)用操作系統(tǒng)的Socket庫,因此下文的代碼實例雖然采用Java語言,但是希望各位讀者注意: 只有語法上抽象與具體的區(qū)別,socket的操作邏輯是完全一致的

但是,我還是得花點口舌啰嗦一下這幾個步驟的一些細節(jié),為了不至于太枯燥,接下來將這4個步驟和BIO一起講解。

3. 阻塞I/O(Blocking I/O,BIO)

我們先從比較簡單的客戶端開始談起。

3.1 客戶端的socket流程

public class BlockingClient {
    public static void main(String[] args) {

        try {
            // 創(chuàng)建套接字 & 建立連接
            Socket socket = new Socket("localhost", 8099);
            // 向服務端寫數(shù)據(jù)
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("我是客戶端,收到請回答?。\n");
            bufferedWriter.flush();

            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line = bufferedReader.readLine();
            System.out.println("收到服務端返回的數(shù)據(jù):" + line);
        } catch (IOException e) {
            // 錯誤處理
        }
    }
}

上面展示了一段非常簡單的Java BIO的客戶端代碼,相信你們一定不會感到陌生,接下來我們一點點分析客戶端的socket操作究竟做了什么。

Socket socket = new Socket("localhost", 8099);

雖然只是簡單的一行語句,但是其中包含了兩個步驟,分別是創(chuàng)建套接字、建立連接,等價于下面兩行偽代碼:

<描述符> = socket(<使用IPv4>, <使用TCP>, ...);
connect(<描述符>, <服務器IP地址和端口號>, ...);

注意:

文中會出現(xiàn)多個關于*ocket的術語,比如Socket庫,就是操作系統(tǒng)提供的庫函數(shù);socket組件就是Socket庫中和socket相關的程序的統(tǒng)稱;socket()函數(shù)以及socket(或稱:套接字)就是接下來要講的內(nèi)容,我會盡量在描述過程中不產(chǎn)生混淆,大家注意根據(jù)上下文進行辨析。

3.1.1 何為socket?

上文已經(jīng)說了,邏輯管道存在的前提是需要各自先創(chuàng)建socket(就好比你打電話之前得先有手機),然后將兩個socket進行關聯(lián)。客戶端創(chuàng)建socket非常簡單,只需要調(diào)用Socket庫中的socket組件的socket()函數(shù)就可以了。

<描述符> = socket(<使用IPv4>, <使用TCP>, ...);

客戶端代碼調(diào)用socket()函數(shù)向協(xié)議棧申請創(chuàng)建socket,協(xié)議棧會根據(jù)你的參數(shù)來決定socket是IPv4還是IPv6,是TCP還是UDP。除此之外呢?

基本的臟活累活都是協(xié)議棧完成的,協(xié)議棧想傳遞消息總得知道目的IP和端口吧,要是你用的是TCP協(xié)議,你甚至還得記錄每個包的發(fā)送時間以及每個包是否收到回復,否則TCP的超時重傳就不會正常工作。。。等等。。。

因此,協(xié)議棧會申請一塊內(nèi)存空間,在其中存放諸如此類的各種控制信息,協(xié)議棧就是根據(jù)這些控制信息來工作的,這些控制信息我們就可以理解為是socket的實體。怎么樣,是不是之前感覺虛無縹緲的socket突然鮮活了起來?

我們看一個更鮮活的例子,我在本級上執(zhí)行netstat -anop命令,得到的每一行信息我們就可以理解為是一個socket,我們重點看一下下圖中標注的兩條。

圖片

這兩條都是redis-server的socket信息,第1條表示redis-server服務正在IP為127.0.0.1,端口為6379的主機上等待遠程客戶端連接,因為Foreign address為0.0.0.0:*,表示通信還未開始,IP無法確定,因此State為LISTEN狀態(tài);第2條表示redis-server服務已經(jīng)建立了與IP為127.0.0.1的客戶端之間的連接,且客戶端使用49968的端口號,目前該socket的狀態(tài)為ESTABLISHED

協(xié)議棧創(chuàng)建完socket之后,會返回一個描述符給應用程序。描述符用來識別不同的socket,可以將描述符理解成某個socket的編號,就好比你去洗澡的時候,前臺會發(fā)給你一個手牌,原理差不多。

之后對socket進行的任何操作,只要我們出示自己的手牌,啊呸,描述符,協(xié)議棧就能知道我們想通過哪個socket進行數(shù)據(jù)收發(fā)了。

圖片

描述符就是socket的號碼牌

至于為什么不直接返回socket的內(nèi)存地址以及其他細節(jié),可以參考我之前寫的文章《2>&1到底是什么意思》

3.1.2 何為連接?

connect(<描述符>, <服務器IP地址和端口號>, ...);

socket剛創(chuàng)建的時候,里邊沒啥有用的信息,別說自己即將通信的對象長啥樣了,就是叫啥,現(xiàn)在在哪兒也不知道,更別提協(xié)議棧,自然是啥也知道!

因此,第1件事情就是應用程序需要把服務器的IP地址端口號告訴協(xié)議棧,有了街道和門牌號,接下來協(xié)議棧就可以去找服務器了。

對于服務器也是一樣的情況,服務器也有自己的socket,在接收到客戶端的信息的同時,服務器也得知道客戶端的IP端口號啊,要不然只能單線聯(lián)系了。因此對客戶端做的第1件事情就有了要求,必須把客戶端自己的IP以及端口號告知服務器,然后兩者就可以愉快的聊天了。

這就是 3次握手 。

一句話概括連接的含義: 連接實際上是通信的雙方交換控制信息,并將必要的控制信息保存在各自的socket中的過程

連接過后,每個socket就被4個信息唯一標識,通常我們稱為四元組:

圖片

socket四元組

趁熱打鐵,我們趕緊再說一說服務器端創(chuàng)建socket以及接受連接的過程。

3.2 服務端的socket流程

public class BIOServerSocket {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(8099);
            System.out.println("啟動服務:監(jiān)聽端口:8099");
            // 等待客戶端的連接過來,如果沒有連接過來,就會阻塞
            while (true) {
                // 表示阻塞等待監(jiān)聽一個客戶端連接,返回的socket表示連接的客戶端信息
                Socket socket = serverSocket.accept(); 
                System.out.println("客戶端:" + socket.getPort());
                // 表示獲取客戶端的請求報文
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 讀操作也是阻塞的
                String clientStr = bufferedReader.readLine();
                System.out.println("收到客戶端發(fā)送的消息:" + clientStr);

                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                bufferedWriter.write("ok\\n");
                bufferedWriter.flush();
            }
        } catch (IOException e) {
            // 錯誤處理
        } finally {
            // 其他處理
        }
    }
}

上面一段是非常簡單的Java BIO的服務端代碼,代碼的含義就是:

  1. 創(chuàng)建socket;
  2. 將socket設置為等待連接狀態(tài);
  3. 接受客戶端連接;
  4. 收發(fā)數(shù)據(jù)。

這些步驟調(diào)用的底層代碼的偽代碼如下:

// 創(chuàng)建socket
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 多路復用
    +關注

    關注

    0

    文章

    36

    瀏覽量

    25515
  • BIO
    BIO
    +關注

    關注

    0

    文章

    6

    瀏覽量

    9358
收藏 人收藏

    評論

    相關推薦

    小米5的那顆核心,究竟有多強

    處理器究竟有多強呢?之前,有消息說三星Note4S-LTE將成為世界上第一臺使用驍龍810處理器的手機。三星在Note4的Advance版本,之所以舍棄自家投入巨大的20nm工藝Exyons5433
    發(fā)表于 06-01 19:35

    Redis Stream應用案例

    的基本使用介紹和設計理念可以看我之前的一篇文章(Redis Stream簡介)。Redis Stream本質(zhì)是在Redis內(nèi)核(非
    發(fā)表于 06-26 17:15

    液晶PC與液晶電視究竟有什么區(qū)別?

    為什么要選擇液晶?液晶PC與液晶電視究竟有什么區(qū)別?如何選擇液晶PC與液晶電視?
    發(fā)表于 06-07 06:13

    請問一下RFID與NFC究竟有什么關系?

    RFID與NFC究竟有什么關系?
    發(fā)表于 06-15 07:06

    面向列的HBase存儲結(jié)構(gòu)究竟有什么樣的不同之處呢?

    HBase是什么?HBase的存儲結(jié)構(gòu)究竟是怎樣的呢?面向列的HBase存儲結(jié)構(gòu)究竟有什么樣的不同之處呢?
    發(fā)表于 06-16 06:52

    請問一下芯片制造究竟有多難?

    請問一下芯片制造究竟有多難?
    發(fā)表于 06-18 06:53

    PCI-E4.0究竟有什么優(yōu)勢?

    PCI-E4.0究竟有什么優(yōu)勢?PCI-E究竟指的是什么呢?
    發(fā)表于 06-18 06:54

    內(nèi)存時序究竟有多重要呢?究竟該如何去選擇內(nèi)存條呢?

    內(nèi)存時序究竟有多重要呢?究竟該如何去選擇內(nèi)存條呢?DDR內(nèi)存時序是高一些好還是低一些好?
    發(fā)表于 06-18 08:20

    定時器中斷類型探究 精選資料分享

     一直在用的stm32定時器的中斷都是TIM_IT_Update更新中斷,也沒問為什么,直到碰到有人使用TIM_IT_CC1中斷,才想到這定時器的中斷類型究竟有什么區(qū)別,都怪當時學習stm32的時候
    發(fā)表于 08-13 06:28

    OpenPLC開源工業(yè)控制器究竟有何用處

    OpenPLC開源工業(yè)控制器有哪些優(yōu)點?OpenPLC開源工業(yè)控制器有哪些功能?OpenPLC開源工業(yè)控制器究竟有何用處?
    發(fā)表于 09-02 07:42

    華為榮耀Magic今日發(fā)布:“未來”手機究竟有多強?

    華為榮耀即將在12月16日發(fā)布最新的“未來”手機magic,關于這款手機的爆料在今日已經(jīng)鋪天蓋地,今天,小編將為大家整理一下,給大家一個榮耀Magic的基本判斷,看看這款旗艦究竟有多強力!
    發(fā)表于 12-16 09:34 ?3310次閱讀

    ibm的2nm芯片究竟有多強 2nm芯片對續(xù)航的影響

    全球首顆2nm芯片的問世對半導體行業(yè)影響重大,IBM通過與AMD、三星及GlobalFoundries等多家公司的合作,最終抵達了2nm芯片制程的節(jié)點,推出了2nm的測試芯片。那么這顆芯片究竟有多強呢?它對續(xù)航的影響又有多大呢?
    的頭像 發(fā)表于 06-23 09:35 ?2038次閱讀

    Molex莫仕連接器的功能究竟有多強大?看他們的行業(yè)應用你就知道了!

    KOYUELEC光與電子:Molex莫仕連接器的功能究竟有多強大?看他們的行業(yè)應用你就知道了!
    的頭像 發(fā)表于 12-31 12:30 ?1w次閱讀

    探究Redis網(wǎng)絡模型究竟有多強大(中)

    創(chuàng)建socket這一步和客戶端沒啥區(qū)別,不同的是這個socket我們稱之為 **等待連接socket(或監(jiān)聽socket)** 。 #### 3.2.2 綁定端口號 `bind()`函數(shù)會將端口號寫入
    的頭像 發(fā)表于 03-03 09:49 ?326次閱讀
    <b class='flag-5'>探究</b><b class='flag-5'>Redis</b><b class='flag-5'>網(wǎng)絡</b><b class='flag-5'>模型</b><b class='flag-5'>究竟有多強大</b>(中)

    探究Redis網(wǎng)絡模型究竟有多強大(下)

    接下來的非阻塞IO我們只抓主要矛盾,其余參考BIO即可。 如果你看過其他介紹非阻塞IO的文
    的頭像 發(fā)表于 03-03 09:50 ?371次閱讀
    <b class='flag-5'>探究</b><b class='flag-5'>Redis</b><b class='flag-5'>網(wǎng)絡</b><b class='flag-5'>模型</b><b class='flag-5'>究竟有多強大</b>(下)