您好,登錄后才能下訂單哦!
這篇文章主要講解了“Redis的事件驅動模型是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Redis的事件驅動模型是什么”吧!
使用 Socket 模型實現網絡通信時,需要經過創建 Socket、監聽端口、處理連接和讀寫請求等多個步驟,現在我們就來具體了解下這些步驟中的關鍵操作,以此幫助我們分析 Socket 模型中的不足。
首先,當我們需要讓服務器端和客戶端進行通信時,可以在服務器端通過以下三步,來創建監聽客戶端連接的監聽套接字(Listening Socket):
調用 socket 函數,創建一個套接字。我們通常把這個套接字稱為主動套接字(Active Socket);
調用 bind 函數,將主動套接字和當前服務器的 IP 和監聽端口進行綁定;
調用 listen 函數,將主動套接字轉換為監聽套接字,開始監聽客戶端的連接。
在完成上述三步之后,服務器端就可以接收客戶端的連接請求了。為了能及時地收到客戶端的連接請求,我們可以運行一個循環流程,在該流程中調用 accept 函數,用于接收客戶端連接請求。
這里你需要注意的是,accept 函數是阻塞函數,也就是說,如果此時一直沒有客戶端連接請求,那么,服務器端的執行流程會一直阻塞在 accept 函數。一旦有客戶端連接請求到達,accept 將不再阻塞,而是處理連接請求,和客戶端建立連接,并返回已連接套接字(Connected Socket)。
最后,服務器端可以通過調用 recv 或 send 函數,在剛才返回的已連接套接字上,接收并處理讀寫請求,或是將數據發送給客戶端。
代碼:
listenSocket = socket(); //調用socket系統調用創建一個主動套接字 bind(listenSocket); //綁定地址和端口 listen(listenSocket); //將默認的主動套接字轉換為服務器使用的被動套接字,也就是監聽套接字 while(1) { //循環監聽是否有客戶端連接請求到來 connSocket = accept(listenSocket);//接受客戶端連接 recv(connSocket);//從客戶端讀取數據,只能同時處理一個客戶端 send(connSocket);//給客戶端返回數據,只能同時處理一個客戶端 }
不過,從上述代碼中,你可能會發現,雖然它能夠實現服務器端和客戶端之間的通信,但是程序每調用一次 accept 函數,只能處理一個客戶端連接。因此,如果想要處理多個并發客戶端的請求,我們就需要使用多線程,來處理通過 accept 函數建立的多個客戶端連接上的請求。
使用這種方法后,我們需要在 accept 函數返回已連接套接字后,創建一個線程,并將已連接套接字傳遞給創建的線程,由該線程負責這個連接套接字上后續的數據讀寫。同時,服務器端的執行流程會再次調用 accept 函數,等待下一個客戶端連接。
多線程:
listenSocket = socket(); //調用socket系統調用創建一個主動套接字 bind(listenSocket); //綁定地址和端口 listen(listenSocket); //將默認的主動套接字轉換為服務器使用的被動套接字,也就是監聽套接字 while(1) { //循環監聽是否有客戶端連接請求到來 connSocket = accept(listenSocket);//接受客戶端連接 pthread_create(processData, connSocket);//創建新線程對已連接套接字進行處理 } processData(connSocket){ recv(connSocket);//從客戶端讀取數據,只能同時處理一個客戶端 send(connSocket);//給客戶端返回數據,只能同時處理一個客戶端 }
雖然這種方法能提升服務器端的并發處理能力,但是,Redis 的主執行流程是由一個線程在執行,無法使用多線程的方式來提升并發處理能力。所以,該方法對redis并不起作用。
還有沒有什么其他方法,能幫助 Redis 提升并發客戶端的處理能力呢?這就要用到操作系統提供的IO多路復用功能。在基本的 Socket 編程模型中,accept 函數只能在一個監聽套接字上監聽客戶端的連接,recv 函數也只能在一個已連接套接字上,等待客戶端發送的請求。
因為 Linux 操作系統在實際應用中比較廣泛,所以這節課,我們主要來學習 Linux 上的 IO 多路復用機制。Linux 提供的 IO 多路復用機制主要有三種,分別是 select、poll 和 epoll。下面,我們就分別來學習下這三種機制的實現思路和使用方法。然后,我們再來看看,為什么 Redis 通常是選擇使用 epoll 這種機制來實現網絡通信。
首先,我們來了解下 select 機制的編程模型。
不過在具體學習之前,我們需要知道,對于一種 IO 多路復用機制來說,我們需要掌握哪些要點,這樣可以幫助我們快速抓住不同機制的聯系與區別。其實,當我們學習 IO 多路復用機制時,我們需要能回答以下問題:第一,多路復用機制會監聽套接字上的哪些事件?第二,多路復用機制可以監聽多少個套接字?第三,當有套接字就緒時,多路復用機制要如何找到就緒的套接字?
select 機制中的一個重要函數就是 select 函數。對于 select 函數來說,它的參數包括監聽的文件描述符數量__nfds、、被監聽描述符的三個集合readfds、writefds、exceptfds,以及監聽時阻塞等待的超時時長timeout。select函數原型:
int select(int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
這里你需要注意的是,Linux 針對每一個套接字都會有一個文件描述符,也就是一個非負整數,用來唯一標識該套接字。所以,在多路復用機制的函數中,Linux 通常會用文件描述符作為參數。有了文件描述符,函數也就能找到對應的套接字,進而進行監聽、讀寫等操作。
select函數三個參數表示的是,被監聽描述符的集合,其實就是被監聽套接字的集合。那么,為什么會有三個集合呢?
剛才提出的第一個問題相關,也就是多路復用機制會監聽套接字上的哪些事件。select 函數使用三個集合,表示監聽的三類事件,分別是讀數據事件,寫數據事件,異常事件。
我們進一步可以看到,參數 readfds、writefds 和 exceptfds 的類型是 fd_set 結構體,它主要定義部分如下所示。其中,fd_mask類型是 long int 類型的別名,__FD_SETSIZE 和 __NFDBITS 這兩個宏定義的大小默認為 1024 和 32。
所以,fd_set 結構體的定義,其實就是一個 long int 類型的數組,該數組中一共有 32 個元素(1024/32=32),每個元素是 32 位(long int 類型的大小),而每一位可以用來表示一個文件描述符的狀態。了解了 fd_set 結構體的定義,我們就可以回答剛才提出的第二個問題了。select 函數對每一個描述符集合,都可以監聽 1024 個描述符。
首先,我們在調用 select 函數前,可以先創建好傳遞給 select 函數的描述符集合,然后再創建監聽套接字。而為了讓創建的監聽套接字能被 select 函數監控,我們需要把這個套接字的描述符加入到創建好的描述符集合中。
然后,我們就可以調用 select 函數,并把創建好的描述符集合作為參數傳遞給 select 函數。程序在調用 select 函數后,會發生阻塞。而當 select 函數檢測到有描述符就緒后,就會結束阻塞,并返回就緒的文件描述符個數。
那么此時,我們就可以在描述符集合中查找哪些描述符就緒了。然后,我們對已就緒描述符對應的套接字進行處理。比如,如果是 readfds 集合中有描述符就緒,這就表明這些就緒描述符對應的套接字上,有讀事件發生,此時,我們就在該套接字上讀取數據。
而因為 select 函數一次可以監聽 1024 個文件描述符的狀態,所以 select 函數在返回時,也可能會一次返回多個就緒的文件描述符。這樣一來,我們就可以使用一個循環流程,依次對就緒描述符對應的套接字進行讀寫或異常處理操作。
select函數有兩個不足
首先,select 函數對單個進程能監聽的文件描述符數量是有限制的,它能監聽的文件描述符個數由 __FD_SETSIZE 決定,默認值是 1024。
其次,當 select 函數返回后,我們需要遍歷描述符集合,才能找到具體是哪些描述符就緒了。這個遍歷過程會產生一定開銷,從而降低程序的性能。
poll 機制的主要函數是 poll 函數,我們先來看下它的原型定義,如下所示:
int poll(struct pollfd *__fds, nfds_t __nfds, int __timeout)
其中,參數 *__fds 是 pollfd 結構體數組,參數 __nfds 表示的是 *__fds 數組的元素個數,而 __timeout 表示 poll 函數阻塞的超時時間。
pollfd 結構體里包含了要監聽的描述符,以及該描述符上要監聽的事件類型。這個我們可以從 pollfd 結構體的定義中看出來,如下所示。pollfd 結構體中包含了三個成員變量 fd、events 和 revents,分別表示要監聽的文件描述符、要監聽的事件類型和實際發生的事件類型。
pollfd 結構體中要監聽和實際發生的事件類型,是通過以下三個宏定義來表示的,分別是 POLLRDNORM、POLLWRNORM 和 POLLERR,它們分別表示可讀、可寫和錯誤事件。
了解了 poll 函數的參數后,我們來看下如何使用 poll 函數完成網絡通信。這個流程主要可以分成三步:
第一步,創建 pollfd 數組和監聽套接字,并進行綁定;
第二步,將監聽套接字加入 pollfd 數組,并設置其監聽讀事件,也就是客戶端的連接請求;
第三步,循環調用 poll 函數,檢測 pollfd 數組中是否有就緒的文件描述符。
而在第三步的循環過程中,其處理邏輯又分成了兩種情況:
如果是連接套接字就緒,這表明是有客戶端連接,我們可以調用 accept 接受連接,并創建已連接套接字,并將其加入 pollfd 數組,并監聽讀事件;
如果是已連接套接字就緒,這表明客戶端有讀寫請求,我們可以調用 recv/send 函數處理讀寫請求。
其實,和 select 函數相比,poll 函數的改進之處主要就在于,它允許一次監聽超過 1024 個文件描述符。但是當調用了 poll 函數后,我們仍然需要遍歷每個文件描述符,檢測該描述符是否就緒,然后再進行處理。
首先,epoll 機制是使用 epoll_event 結構體,來記錄待監聽的文件描述符及其監聽的事件類型的,這和 poll 機制中使用 pollfd 結構體比較類似。
那么,對于 epoll_event 結構體來說,其中包含了 epoll_data_t 聯合體變量,以及整數類型的 events 變量。epoll_data_t 聯合體中有記錄文件描述符的成員變量 fd,而 events 變量會取值使用不同的宏定義值,來表示 epoll_data_t 變量中的文件描述符所關注的事件類型,比如一些常見的事件類型包括以下這幾種。
EPOLLIN:讀事件,表示文件描述符對應套接字有數據可讀。
EPOLLOUT:寫事件,表示文件描述符對應套接字有數據要寫。
EPOLLERR:錯誤事件,表示文件描述符對于套接字出錯。
在使用 select 或 poll 函數的時候,創建好文件描述符集合或 pollfd 數組后,就可以往數組中添加我們需要監聽的文件描述符。
但是對于 epoll 機制來說,我們則需要先調用 epoll_create 函數,創建一個 epoll 實例。這個 epoll 實例內部維護了兩個結構,分別是記錄要監聽的文件描述符和已經就緒的文件描述符,,而對于已經就緒的文件描述符來說,它們會被返回給用戶程序進行處理。
所以,我們在使用 epoll 機制時,就不用像使用 select 和 poll 一樣,遍歷查詢哪些文件描述符已經就緒了。這樣一來, epoll 的效率就比 select 和 poll 有了更高的提升。
在創建了 epoll 實例后,我們需要再使用 epoll_ctl 函數,給被監聽的文件描述符添加監聽事件類型,以及使用 epoll_wait 函數獲取就緒的文件描述符。
了解了 epoll 函數的使用方法了。實際上,也正是因為 epoll 能自定義監聽的描述符數量,以及可以直接返回就緒的描述符,Redis 在設計和實現網絡通信框架時,就基于 epoll 機制中的 epoll_create、epoll_ctl 和 epoll_wait 等函數和讀寫事件,進行了封裝開發,實現了用于網絡通信的事件驅動框架,從而使得 Redis 雖然是單線程運行,但是仍然能高效應對高并發的客戶端訪問。
Reactor 模型就是網絡服務器端用來處理高并發網絡 IO 請求的一種編程模型,模型特征:
三類處理事件,即連接事件、寫事件、讀事件;
三個關鍵角色,即 reactor、acceptor、handler。
Reactor 模型處理的是客戶端和服務器端的交互過程,而這三類事件正好對應了客戶端和服務器端交互過程中,不同類請求在服務器端引發的待處理事件:
當一個客戶端要和服務器端進行交互時,客戶端會向服務器端發送連接請求,以建立連接,這就對應了服務器端的一個鏈接事件
一旦連接建立后,客戶端會給服務器端發送讀請求,以便讀取數據。服務器端在處理讀請求時,需要向客戶端寫回數據,這對應了服務器端的寫事件
無論客戶端給服務器端發送讀或寫請求,服務器端都需要從客戶端讀取請求內容,所以在這里,讀或寫請求的讀取就對應了服務器端的讀事件
三個關鍵角色:
首先,連接事件由 acceptor 來處理,負責接收連接;acceptor 在接收連接后,會創建 handler,用于網絡連接上對后續讀寫事件的處理;
其次,讀寫事件由 handler 處理;
最后,在高并發場景中,連接事件、讀寫事件會同時發生,所以,我們需要有一個角色專門監聽和分配事件,這就是 reactor 角色。當有連接請求時,reactor 將產生的連接事件交由 acceptor 處理;當有讀寫請求時,reactor 將讀寫事件交由 handler 處理。
那么,現在我們已經知道,這三個角色是圍繞事件的監聽、轉發和處理來進行交互的,那么在編程時,我們又該如何實現這三者的交互呢?這就離不開事件驅動。
所謂的事件驅動框架,就是在實現 Reactor 模型時,需要實現的代碼整體控制邏輯。簡單來說,事件驅動框架包括了兩部分:一是事件初始化,二事件捕獲,分化和處理主循環。
事件初始化是在服務器程序啟動時就執行的,它的作用主要是創建需要監聽的事件類型,以及該類事件對應的 handler。而一旦服務器完成初始化后,事件初始化也就相應完成了,服務器程序就需要進入到事件捕獲、分發和處理的主循環中。
用while循環來作為這個主循環。然后在這個主循環中,我們需要捕獲發生的事件、判斷事件類型,并根據事件類型,調用在初始化時創建好的事件 handler 來實際處理事件。
比如說,當有連接事件發生時,服務器程序需要調用 acceptor 處理函數,創建和客戶端的連接。而當有讀事件發生時,就表明有讀或寫請求發送到了服務器端,服務器程序就要調用具體的請求處理函數,從客戶端連接中讀取請求內容,進而就完成了讀事件的處理。
Reactor 模型的基本工作機制:客戶端的不同類請求會在服務器端觸發連接、讀、寫三類事件,這三類事件的監聽、分發和處理又是由 reactor、acceptor、handler 三類角色來完成的,然后這三類角色會通過事件驅動框架來實現交互和事件處理。
感謝各位的閱讀,以上就是“Redis的事件驅動模型是什么”的內容了,經過本文的學習后,相信大家對Redis的事件驅動模型是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。