網站首頁 編程語言 正文
Redis 作為一個 Client-Server 架構的數據庫,其源碼中少不了用來實現網絡通信的部分。而你應該也清楚,通常系統實現網絡通信的基本方法是使用Socket編程模型,,包括創建 Socket、監聽端口、處理連接請求和讀寫請求。但是,由于基本的 Socket 編程模型一次只能處理一個客戶端連接上的請求,所以當要處理高并發請求時,一種方案就是使用多線程,讓每個線程負責處理一個客戶端的請求。
而 Redis 負責客戶端請求解析和處理的線程只有一個,那么如果直接采用基本 Socket 模型,就會影響 Redis 支持高并發的客戶端訪問。
因此,為了實現高并發的網絡通信,我們常用的 Linux 操作系統,就提供了 select、poll 和 epoll 三種編程模型,而在 Linux 上運行的 Redis,通常就會采用其中的epoll模型來進行網絡通訊。
為啥 Redis 通常會選擇 epoll 模型呢?這三種編程模型之間有什么區別?
要想理解 select、poll 和 epoll 的優勢,我們需要有個對比基礎,也就是基本的 Socket 編程模型。所以接下來,我們就先來了解下基本的 Socket 編程模型,以及它的不足之處。
為什么 Redis 不使用基本的 Socket 編程模型?
使用 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 和 poll 機制實現 IO 多路復用
首先,我們來了解下 select 機制的編程模型。
不過在具體學習之前,我們需要知道,對于一種 IO 多路復用機制來說,我們需要掌握哪些要點,這樣可以幫助我們快速抓住不同機制的聯系與區別。其實,當我們學習 IO 多路復用機制時,我們需要能回答以下問題:第一,多路復用機制會監聽套接字上的哪些事件?第二,多路復用機制可以監聽多少個套接字?第三,當有套接字就緒時,多路復用機制要如何找到就緒的套接字?
select機制
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 函數后,會發生阻塞。而當 select 函數檢測到有描述符就緒后,就會結束阻塞,并返回就緒的文件描述符個數。
那么此時,我們就可以在描述符集合中查找哪些描述符就緒了。然后,我們對已就緒描述符對應的套接字進行處理。比如,如果是 readfds 集合中有描述符就緒,這就表明這些就緒描述符對應的套接字上,有讀事件發生,此時,我們就在該套接字上讀取數據。
而因為 select 函數一次可以監聽 1024 個文件描述符的狀態,所以 select 函數在返回時,也可能會一次返回多個就緒的文件描述符。這樣一來,我們就可以使用一個循環流程,依次對就緒描述符對應的套接字進行讀寫或異常處理操作。
select函數有兩個不足
首先,select 函數對單個進程能監聽的文件描述符數量是有限制的,它能監聽的文件描述符個數由 __FD_SETSIZE 決定,默認值是 1024。
其次,當 select 函數返回后,我們需要遍歷描述符集合,才能找到具體是哪些描述符就緒了。這個遍歷過程會產生一定開銷,從而降低程序的性能。
poll機制
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 機制是使用 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 模型的工作機制
Reactor 模型就是網絡服務器端用來處理高并發網絡 IO 請求的一種編程模型,模型特征:
- 三類處理事件,即連接事件、寫事件、讀事件;
- 三個關鍵角色,即 reactor、acceptor、handler。
Reactor 模型處理的是客戶端和服務器端的交互過程,而這三類事件正好對應了客戶端和服務器端交互過程中,不同類請求在服務器端引發的待處理事件:
當一個客戶端要和服務器端進行交互時,客戶端會向服務器端發送連接請求,以建立連接,這就對應了服務器端的一個鏈接事件
一旦連接建立后,客戶端會給服務器端發送讀請求,以便讀取數據。服務器端在處理讀請求時,需要向客戶端寫回數據,這對應了服務器端的寫事件
無論客戶端給服務器端發送讀或寫請求,服務器端都需要從客戶端讀取請求內容,所以在這里,讀或寫請求的讀取就對應了服務器端的讀事件
三個關鍵角色:
首先,連接事件由 acceptor 來處理,負責接收連接;acceptor 在接收連接后,會創建 handler,用于網絡連接上對后續讀寫事件的處理;
其次,讀寫事件由 handler 處理;
最后,在高并發場景中,連接事件、讀寫事件會同時發生,所以,我們需要有一個角色專門監聽和分配事件,這就是 reactor 角色。當有連接請求時,reactor 將產生的連接事件交由 acceptor 處理;當有讀寫請求時,reactor 將讀寫事件交由 handler 處理。
那么,現在我們已經知道,這三個角色是圍繞事件的監聽、轉發和處理來進行交互的,那么在編程時,我們又該如何實現這三者的交互呢?這就離不開事件驅動。
所謂的事件驅動框架,就是在實現 Reactor 模型時,需要實現的代碼整體控制邏輯。簡單來說,事件驅動框架包括了兩部分:一是事件初始化,二事件捕獲,分化和處理主循環。
事件初始化是在服務器程序啟動時就執行的,它的作用主要是創建需要監聽的事件類型,以及該類事件對應的 handler。而一旦服務器完成初始化后,事件初始化也就相應完成了,服務器程序就需要進入到事件捕獲、分發和處理的主循環中。
用while循環來作為這個主循環。然后在這個主循環中,我們需要捕獲發生的事件、判斷事件類型,并根據事件類型,調用在初始化時創建好的事件 handler 來實際處理事件。
比如說,當有連接事件發生時,服務器程序需要調用 acceptor 處理函數,創建和客戶端的連接。而當有讀事件發生時,就表明有讀或寫請求發送到了服務器端,服務器程序就要調用具體的請求處理函數,從客戶端連接中讀取請求內容,進而就完成了讀事件的處理。
Reactor 模型的基本工作機制:客戶端的不同類請求會在服務器端觸發連接、讀、寫三類事件,這三類事件的監聽、分發和處理又是由 reactor、acceptor、handler 三類角色來完成的,然后這三類角色會通過事件驅動框架來實現交互和事件處理。
原文鏈接:https://juejin.cn/post/7102692279076257806
相關推薦
- 2022-06-21 C語言實現順序表的全操作詳解_C 語言
- 2022-08-13 Redis 性能影響 - 異步機制和響應延遲
- 2023-04-18 Android粒子線條效果實現過程與代碼_Android
- 2022-09-10 PyCharm新建.py文件時默認添加信息的實現_python
- 2022-10-24 Golang設計模式工廠模式實戰寫法示例詳解_Golang
- 2022-08-16 C#?IEnumerator枚舉器的具體使用_C#教程
- 2022-04-01 Python?eval()?函數看這一篇就夠了_python
- 2022-07-30 Redis?keys命令的具體使用_Redis
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支