概述
這次我們來談?wù)劸W(wǎng)絡(luò)IO模型,網(wǎng)絡(luò)IO是系統(tǒng)底層網(wǎng)絡(luò)操作的一環(huán),不管做系統(tǒng)的哪一層,基本都會涉及到網(wǎng)絡(luò)IO模型,從架構(gòu)設(shè)計、中間件設(shè)計、分布式系統(tǒng)到代碼排錯,很可能都會涉及到網(wǎng)絡(luò)IO模型。我打算先聊聊樸素的網(wǎng)絡(luò)IO 模型里的各個模型,再探討一下并發(fā)模型,然后我們來討論一下高性能網(wǎng)絡(luò)IO模型,高性能網(wǎng)絡(luò)IO里我們仔細討論一下隨處可見的 reactor 模型。
網(wǎng)絡(luò)IO模型
網(wǎng)絡(luò)IO操作是操作系統(tǒng)的基本操作,基本體現(xiàn)在網(wǎng)絡(luò)套接字、網(wǎng)絡(luò)屬性的操作API上,還有很多與文件操作等等的API也會經(jīng)常用到。
網(wǎng)絡(luò)IO可以籠統(tǒng)地概述為一個串行的過程:
- 初始化服務(wù)端地址、套接字等參數(shù)
- 綁定套接字與對應(yīng)的服務(wù)端地址
- 建立服務(wù)端監(jiān)聽連接
- 若有客戶端連接,接收客戶端連接
- 通過客戶端套接字與客戶端進行讀或?qū)懡换?/li>
- 關(guān)閉客戶端連接
這個簡單模型是同步的,即每個操作都是依次執(zhí)行,實際上目前常見的開源軟件如netty、nginx、swoole等的網(wǎng)絡(luò)處理里,網(wǎng)絡(luò)IO基本模型都是同步的,只是在整個服務(wù)端處理連接的流程中是異步的。如nginx會將accept事件和讀寫事件放到隊列里,異步去處理讀寫操作。
下面我們分別來走進每個網(wǎng)絡(luò)IO模型。
阻塞IO(blocking I/O)
阻塞的定義可能每個人的說法都不一樣,一般來說阻塞是一個進程狀態(tài),若一個任務(wù)在執(zhí)行過程中被操作系統(tǒng)剝奪了CPU控制權(quán),就可以認為該任務(wù)被阻塞了。
上述籠統(tǒng)的網(wǎng)絡(luò)IO流程就是一個阻塞的IO模型,我們可以通過一個簡單的實例來描述一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
void handleError(char *str) {
printf("%s, strerror: %s\n", str, strerror(errno));
exit(errno);
}
int main(int argc, char *argv[]) {
// 初始化參數(shù)
int serverFd;
int clientFd;
int len;
struct sockaddr_in serverAddr;
struct sockaddr_in clientAddr;
socklen_t addrSize;
char buf[BUFSIZ];
char writeBuf[1024];
// 設(shè)置服務(wù)端地址等參數(shù)
serverAddr.sin_family = AF_INET; // IP通信
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服務(wù)器IP
serverAddr.sin_port = htons(8000); // 端口
// 初始化服務(wù)器端套接字
if ((serverFd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
handleError("socket error");
}
// 設(shè)置 socket 屬性為非阻塞方式
/*if (fcntl(serverFd, F_SETFL, O_NONBLOCK) == -1) {
handleError("fcntl error");
}*/
// 綁定套接字
if (bind(serverFd, (struct sockaddr *) &serverAddr, sizeof(struct sockaddr)) < 0) {
handleError("bind error");
}
// 監(jiān)聽連接請求-隊列長5
if (listen(serverFd, 5) < 0) {
handleError("listen error");
}
do {
// 接收客戶端連接
addrSize = sizeof(clientAddr);
clientFd = accept(serverFd, (struct sockaddr *) &clientAddr, &addrSize);
if (clientFd == -1) {
handleError("accept error");
/*sleep(1);
printf("處理其他事情…\n"); // 處理其他事情
continue;*/
}
printf("接收客戶端 %s\n", inet_ntoa(clientAddr.sin_addr));
len = send(clientFd, "Welcome!\n", 9, 0);
// 設(shè)置讀為阻塞
if (fcntl(clientFd, F_SETFL, O_RDWR) == -1) {
handleError("fcntl error");
}
// 讀客戶端,并寫服務(wù)端響應(yīng)
while ((len = recv(clientFd, buf, BUFSIZ, 0)) > 0) {
buf[len] = '\0';
printf("客戶端請求:%s", buf);
memset(writeBuf, 0, strlen(writeBuf));
strcat(writeBuf, "服務(wù)端響應(yīng):");
strcat(writeBuf, buf);
strcat(writeBuf, "\0");
if (send(clientFd, writeBuf, strlen(writeBuf), 0) < 0) {
handleError("write error");
}
}
} while (1);
return 0;
}
可以看到對客戶端的 accept 和 recv 都是阻塞的,當(dāng)沒有客戶端連接或者當(dāng)前客戶端沒有寫數(shù)據(jù)的時候,進程會陷入系統(tǒng)調(diào)用,CPU 控制權(quán)是被剝奪的。
我們可以簡單演示一下,服務(wù)端運行起來后:

可以看到服務(wù)端每一次操作后都會停住,也就是阻塞掉,第一個客戶端的每次請求必須要等上一次處理完了才會處理當(dāng)前請求,并且可以看到第二個客戶端發(fā)起連接后服務(wù)端是無響應(yīng)的,連接請求是暫時無法被服務(wù)端 accept 處理的。
關(guān)于阻塞IO,Java 有一個處理庫叫 BIO,有時間我們可以了解一下,基本操作對程序員非常友好。
非阻塞IO(non-blocking I/O)
非阻塞就是無客戶端請求的時候可以去處理的事情,輪訓(xùn)監(jiān)聽客戶端連接。
將上述例子注釋的代碼稍作修改即為一個簡單的同步非阻塞IO實例:
if (fcntl(serverFd, F_SETFL, O_NONBLOCK) == -1) {
handleError("fcntl error");
}
// handleError("accept error");
sleep(1);
printf("處理其他事情…\n"); // 處理其他事情
continue;
效果可參考:

信號驅(qū)動IO(signal-driven I/O)
在信號驅(qū)動式 IO 模型中,程序使用套接口進行信號驅(qū)動 I/O,并安裝信號處理函數(shù),不阻塞進程運行。信號驅(qū)動IO的使用似乎不太常見,我們不作討論了。
異步IO
異步IO目前 Linux 下的支持并不太成熟,Window 下是支持的,但是似乎業(yè)界沒有非常廣泛的應(yīng)用,異步IO我們就也不作過多討論了。后面我們會介紹一下基于異步IO的Proactor模型。
IO多路復(fù)用
多路復(fù)用IO模型也叫事件通知模型,一般理解是操作系統(tǒng)提供的單線程或單進程同時監(jiān)測若干個文件描述符是否可以執(zhí)行IO操作的能力。內(nèi)核一旦發(fā)現(xiàn)某個或某些IO條件準備讀寫,就會通過該能力通知用戶進程。
比如我們現(xiàn)在有 n 個需要讀或?qū)懙?fd ,我們通過某些系統(tǒng) API 將這些 fd 通知給內(nèi)核,內(nèi)核一旦發(fā)現(xiàn)這些 fd 有讀或?qū)懢蜁ㄟ^某些系統(tǒng) API 通知進程,另外常用的操作是管理這個 fd 集合,如往 fd 集合里添加 fd 、刪除 fd 等操作。
上述是對多路復(fù)用IO模型比較籠統(tǒng)的理解,我們知道常見的多路復(fù)用IO模型有 select、poll、epoll、queue 等等,每個IO模型都有其適用的場景,一般我們熟知的高性能IO模型里最常見的是 epoll ,優(yōu)秀的開源高性能網(wǎng)“組件”如 nginx、swoole、netty、goroutine 調(diào)度模型都是 epoll 封裝的。這里我們就不打算單純得學(xué)習(xí) select、epoll 系統(tǒng)調(diào)用API的使用了,一是因為 select 模型在實際使用中場景太少,單純地了解這些系統(tǒng)調(diào)用 API 的使用意義不大,另外一個原因是我們用單進程簡單實現(xiàn)的 select 或者 epoll 模型無法真實應(yīng)用,實際場景漏考慮,也非高性能。對 select、epoll 系統(tǒng) API 的使用我們翻查文檔即可。
后面的 reactor 會用 Java 介紹 epoll 的一般常規(guī)使用,基于Java NIO 寫的輪子穩(wěn)定可用,完善后有實際使用意義。后面我們會站在 nginx 的肩膀上,研究一下 nginx 對 epoll 的使用。
并發(fā)模型
單純地依靠操作系統(tǒng)網(wǎng)絡(luò)IO模型的能力并不能寫出高性能的網(wǎng)絡(luò)IO模型,在寫出高性能IO操作的路上,我們少不了的就是并發(fā)模型,下面我們來簡單了解一下并發(fā)模型。
多進程/多線程
多任務(wù)處理我們應(yīng)該都不會陌生,多任務(wù)處理里最常見的就是多進程或多線程了,PHP 使用的可能是多進程居多,Java 一般使用多線程。
多任務(wù)的處理邏輯里,比較麻煩的應(yīng)該相互通信了,進程間由于是隔離的,所以一般的通信方式就是共享內(nèi)存,當(dāng)然還有管道(pipe)、FIFO(命名管道)、消息隊列(詳見sys/msg.h)、信號量(sys/sem.h)、進程通知信號,或者是網(wǎng)絡(luò)通信等等。但是一般語言支持的進程通信組件不太成熟,所以邏輯寫起來一般比較累人,如果場景需要頻繁地通信,一般可能會用線程或者協(xié)程。
相比較進程,線程的通信會成熟很多,一個全局變量即可通信,Java 還有一些阻塞的隊列操作,使得任務(wù)之間通信會比較簡單。
函數(shù)式
函數(shù)式這里我們就不討論了,網(wǎng)絡(luò)IO領(lǐng)域里函數(shù)式比較少見,Haskell 對網(wǎng)絡(luò)是支持好像也不太好學(xué),后面我們在研究吧。
Actor 模型
暫不討論。
CSP 模型
其實很多語言對 CSP 并發(fā)模型的實現(xiàn)就已經(jīng)使用了 IO 模型了,如:
- go 的 goroutine 調(diào)度對 CSP 的底層實現(xiàn)就是使用 epoll,后面我們可以研究一下 GMP 模型的源碼實現(xiàn)。
- swoole 的 Coroutine 調(diào)度也是對 CSP 模型的實現(xiàn),底層也是使用 epoll,本身也是一個高性能的網(wǎng)絡(luò)IO模型,實現(xiàn)應(yīng)該也是 reactor。
這些巨人的肩膀,我們有機會都可以爬上去一覽眾山。
高性能網(wǎng)絡(luò)模型
我們?nèi)绻直┑貙⒉l(fā)模型和網(wǎng)絡(luò)IO模型進行一個簡單的組合,就可以發(fā)現(xiàn)我們目前業(yè)界常用的高性能網(wǎng)絡(luò)IO的雛形,但是也有一些例外,比如 CSP 模型就是使用 epoll 實現(xiàn)的并發(fā)模型,所以其本身就是一個高性能網(wǎng)絡(luò)IO模型。
同步非阻塞IO+多進程
也叫 PPC (Process Per Connection),傳統(tǒng)的或者是有名的有 Apache 的實現(xiàn),也叫 Apache 模型(據(jù)了解 Apache 好像已經(jīng)舍棄該實現(xiàn))。
基本原理也簡單明了,每個連接都會分到一個進程處理,在并發(fā)不高的情況下,也能實現(xiàn)較高的性能。但是顯而易見的有兩個問題:
- 一是受操作系統(tǒng)可用進程數(shù)的限制,在 LInux 就是可用 fd 的配置,如果系統(tǒng)支持可打開的文件句柄為6W,那么并發(fā)能力可以簡單地認為是6W;
- 二是一旦進程數(shù)量多了,進程切換的性能損耗極大,進而影響系統(tǒng)的響應(yīng)時間、吞吐量。
同步非阻塞IO+多線程
也叫 TPC (Thread Per Connection),與上述的 PPC 模型類似,其中有區(qū)別的是換為線程后,上下文切換開銷會小很多。
- 我們可以自己使用java BIO 和線程池編寫一個實例作實踐,開發(fā)起來也是非常方便的,這里我們就不作展示了。
同步非阻塞IO+CSP模型(多協(xié)程)
上文說過,CSP 模型本身就已經(jīng)是一個高性能IO模型。
在實際應(yīng)用中,以 go 語言為例,由于 go 已經(jīng)對 CSP 模型有實現(xiàn)了,所以一般用 go 編寫的優(yōu)秀的開源中間件里對網(wǎng)絡(luò)IO模型的實現(xiàn)極為簡單,不需要自己操作處理 epoll 的fd,只需要將處理客戶端連接的協(xié)程啟動即可,GMP 調(diào)度器會使用 epoll 調(diào)度該協(xié)程。
比如開源數(shù)據(jù)庫中間件 kingshard 里對于網(wǎng)絡(luò)IO的實現(xiàn):
func (s *Server) Run() error {
s.running = true
// flush counter
go s.flushCounter()
for s.running {
conn, err := s.listener.Accept()
if err != nil {
golog.Error("server", "Run", err.Error(), 0)
continue
}
// 將處理連接協(xié)程交由 GMP 調(diào)度
go s.onConn(conn)
}
return nil
}
怎么樣,很輕松吧。
多路復(fù)用IO+多線程/多進程
多路復(fù)用IO模型是我們?nèi)粘R姷米疃嗟腎O模型,一般會根據(jù)場景使用單進程、多進程或者使用線程實現(xiàn)功能,多路復(fù)用IO模型由于用的人太多,慢慢有人總結(jié)出來一個大家都認可的模型,這個模型就是 reactor 模型,我們詳細地來探討一下這個模型。我們每天都在使用的 nginx、redis、swoole、netty 都是使用這個模型。
Reactor模型
reactor 模型其實由三部分組成,reactor、acceptor、handler。
- reactor 負責(zé)監(jiān)聽和分發(fā)連接和讀寫事件,連接事件交由 acceptor 處理,讀寫事件會交給 handler 處理;
- acceptor 負責(zé)初始化服務(wù)端連接,接收新客戶端連接;
- handler 負責(zé)處理客戶端連接讀和寫;
當(dāng)這三部分和多進程/多線程結(jié)合起來,我們自然就會遇到每個組件要起多少進程的問題,其實每個組件都可以起多個進程。
如遇到很多個服務(wù)端連接(如多個端口)的情況下,起多個 acceptor 是可以的,只是像諸如 nginx、redis 的服務(wù)端場景下,服務(wù)端連接一般不會很多,所以單獨起多個 acceptor 對提升性能其實幫助不大,反而會增加上下文切換開銷;
對于處理事件通知的 reactor ,多個 reactor 可以幫助 nginx 最大地提高服務(wù)端吞吐量,但是在像 redis 這些性能瓶頸是內(nèi)存而非 CPU 的場景下,不用多個 reactor 也是沒問題的;
因為 handler 是處理實際業(yè)務(wù)邏輯讀寫的,所以多進程或單進程需要看業(yè)務(wù)場景;
因此我們發(fā)現(xiàn)不能一概而論地敲定某個 reactor 模型的實現(xiàn)就是高性能的,最合適的其實還是要依據(jù)具體場景。但是因為我們主要關(guān)注網(wǎng)絡(luò)IO的多路復(fù)用,其實就是 epoll 的使用,我們可以分別以單 reactor 和多 reactor 來討論一下業(yè)界的實現(xiàn)。
單 reactor 模型
單進程的單 reactor 方案在實踐中應(yīng)用場景不多,只適用于業(yè)務(wù)處理非常快速的場景,但是其實一般并發(fā)不大的情況下,單進程單 reactor 還是沒問題的。
我們常用的 redis 就是單進程單 reactor 的實現(xiàn),可能因為redis的瓶頸不在 CPU,而在內(nèi)存和其它指標。可以參考極客時間的專欄里的這個圖:

多進程加單 reactor 的設(shè)計可以是多個 acceptor 進程或者多個處理業(yè)務(wù)讀寫的 handler 進程,如果我們遇到的場景是并發(fā)不高,但是業(yè)務(wù)讀寫比較耗時的場景,還是可以考慮單個 reactor +多個 handler 的設(shè)計的,至于 acceptor 根據(jù)情況判斷,不單獨起 acceptor 進程也是很常見的。可以參考下圖:

至于單 reactor 其它設(shè)計就要看我們遇到的場景再作討論了。如果我們使用 Java ,用 NIO 去實現(xiàn)還是很友好很方便的,我們可以自己實踐不同的設(shè)計。
多 reactor 模型
接下來我們重點來研究一下多 reactor 下的實現(xiàn),單進程多 reactor 的實現(xiàn)沒什么意義(也就是一個進程處理多個 epoll ),我們就不討論了。
關(guān)于多進程下的多 reactor,幾乎每個開源組件對于 reactor 模型的實現(xiàn)幾乎都不一樣,我們不妨思考一個自己認為高效的模型,對于這種不同場景下有不同實現(xiàn)的模型,實踐更能出真知。
我們這個實現(xiàn)對 Netty 和 nginx 的模型做了簡單的參考,基本結(jié)構(gòu)可以查看下圖:

acceptor 獨立出來的原因是更好維護,acceptor 的邏輯是接客戶端新連接,并添加連接 fd 到響應(yīng)的 reactor 里。這個 acceptor 可以再拓展為類似 netty 里的 mainReactor ,用于處理鑒權(quán)、分發(fā)請求至 subReactor 等邏輯;
對于 reactor 則根據(jù)配置啟動若干個子線程,一般可以配置為 CPU 核數(shù)一樣,具體邏輯是處理讀寫連接,“分發(fā)”至 handler 處理;
handler 筆者這里決定和 reactor 做到一個線程內(nèi)處理讀寫邏輯,只是模塊是分開的,沒有做成一個 handler 線程池是因為這里會增加很多線程切換的性能損耗。其實即使這樣,讀寫事件和創(chuàng)建新連接事件也是分開進程處理的,不需要擔(dān)心連接事件處理會被阻塞在讀寫事件后。
代碼量簡單且不多,以供我們學(xué)習(xí)使用:
// Acceptor 代碼:
package PlainReactorLoop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.*;
import java.util.Random;
/**
* 連接事件就緒,處理連接事件
*/
class Acceptor implements Runnable {
private Logger logger = LoggerFactory.getLogger(Acceptor.class);
private ServerSocketChannel serverSocketChannel;
Acceptor() {
// 初始化服務(wù)端連接
initServerSocket();
}
public static void start() {
new Thread(new Acceptor()).start();
}
public void run() {
while (!Thread.interrupted()) {
try {
SocketChannel socketChannel = serverSocketChannel.accept();
logger.info("接收到客戶端連接……");
if (socketChannel != null) {
register(socketChannel);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 初始化服務(wù)端連接
*/
private Acceptor initServerSocket() {
try {
// 打開監(jiān)聽信道
serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
// 與本地端口綁定
serverSocketChannel.socket().bind(address);
// 設(shè)置為非阻塞模式
//serverSocketChannel.configureBlocking(false);
logger.info("初始化服務(wù)端連接完成……");
} catch (IOException e) {
e.printStackTrace();
}
return this;
}
/**
* 通知Reactor注冊選擇器, 在注冊過程中指出該信道可以進行Read操作
*/
private void register(SocketChannel socketChannel) {
try {
socketChannel.configureBlocking(false);
// 喚起selector以防鎖未釋放
int selectorIndex = new Random().nextInt(Server.processNum);
Server.selector[selectorIndex].wakeup();
socketChannel.register(Server.selector[selectorIndex], SelectionKey.OP_READ);
logger.info("Acceptor注冊Socket到Reactor Selector" + selectorIndex + "……");
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Reactor 代碼:
package PlainReactorLoop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 等待事件到來,分發(fā)事件處理
*/
class Reactor implements Runnable {
public static final Logger logger = LoggerFactory.getLogger(Reactor.class);
private Integer selectorIndex;
Reactor(Integer selectorIndex) {
this.selectorIndex = selectorIndex;
try {
// 初始化 selector
Server.selector[selectorIndex] = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void start(Integer processNum) {
for (Integer selectorIndex = 0; selectorIndex < processNum; selectorIndex++) {
new Thread(new Reactor(selectorIndex)).start();
}
}
public void run() {
try {
logger.info("Reactor" + this.selectorIndex + " 運行中……");
while (!Thread.interrupted()) {
// 創(chuàng)建選擇器
Server.selector[selectorIndex].select();
logger.info("Reactor" + this.selectorIndex + " 處理IO事件……");
Set<SelectionKey> selected = Server.selector[selectorIndex].selectedKeys();
Iterator<SelectionKey> it = selected.iterator();
while (it.hasNext()) {
dispatch(it.next());
it.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 分發(fā)事件處理
*/
void dispatch(SelectionKey k) {
if (k.isReadable()) {
// 若是IO讀寫事件,調(diào)handler處理
logger.info("Reactor" + this.selectorIndex + " IO讀寫事件分發(fā)……");
SocketChannel socketChannel = (SocketChannel) k.channel();
new Handler(socketChannel).run();
}
}
}
// Handler 代碼:
package PlainReactorLoop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
/**
* 處理讀寫業(yè)務(wù)邏輯
*/
class Handler implements Runnable {
private SocketChannel socketChannel;
private static final Logger logger = LoggerFactory.getLogger(Handler.class);
public Handler(SocketChannel socket) {
socketChannel = socket;
}
public void run() {
try {
logger.info("處理業(yè)務(wù)邏輯……");
String question = read();
String resultMsg = process(question);
write(resultMsg);
} catch (Exception e) {
e.printStackTrace();
}
}
private String read() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead <= 0) {
throw new IOException("客戶端數(shù)據(jù)為空");
}
buffer.flip();
// 將字節(jié)轉(zhuǎn)化為為UTF-16的字符串
String receivedString = Charset.forName("utf-8").newDecoder().decode(buffer).toString();
// 控制臺打印
logger.info("接收到來自" + socketChannel.socket().getRemoteSocketAddress() + "的信息:" + receivedString);
return receivedString;
}
private void write(String resultMsg) throws IOException {
socketChannel.write(ByteBuffer.wrap(resultMsg.getBytes()));
logger.info("回復(fù)" + socketChannel.socket().getRemoteSocketAddress() + "信息:" + resultMsg);
}
/**
* task 業(yè)務(wù)處理
*/
public String process(String question) {
String res = question.replace("?", ".");
return res;
}
}
實現(xiàn)效果一覽:

更詳細的代碼可以參看:
https://github.com/varXinYuan/reactorLoop
關(guān)于 Netty 的參考,拜讀了一下大神對于 NIO 和 netty 的介紹:
關(guān)于 Nginx 進程模型的參考,我們會在下一次具體探討 nginx 源碼里 reactor 模型的實現(xiàn),敬請期待。
異步IO+多進程
Proactor 模型
Proactor 模型要依賴操作系統(tǒng)異步IO的支持,因此并未廣泛應(yīng)用,目前 Windows 下通過 IOCP 實現(xiàn)了真正的異步 I/O,而在 Linux 系統(tǒng)下的 AIO 并不完善,因此在 Linux 下實現(xiàn)高并發(fā)網(wǎng)絡(luò)編程時都是以 Reactor 模式為主。
所以即使 Boost.Asio 號稱實現(xiàn)了 Proactor 模型,其實它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(采用 epoll)模擬出來的異步模型。
Netty 在 Linux 下的 AIO 實現(xiàn)也是使用 epoll 模擬的。
結(jié)語
到這里我們把網(wǎng)絡(luò)IO模型的各個分類、細節(jié)和實現(xiàn)都游覽了一遍。下次我們會站在 nginx 源碼的肩膀上再覽群山,不見不散。