五種網絡模型

https://cloud.tencent.com/developer/article/1586243
https://blog.csdn.net/woaixiaopangniu521/article/details/70279143
https://tech.meituan.com/2016/11/04/nio.html
https://www.jianshu.com/p/dde17c2d2e50

站在服務端的視角,對於一次 Socket 的數據讀取操作流程,網絡數據到達網卡,數據先被拷貝到內核緩衝區中,然後從內核緩衝區拷貝到進程用戶空間。
站在服務端的視角,當一個讀操作發生時,稍微再細化一下,其實會經歷兩個階段。

第一階段:等待數據準備。
例如:recv() 等待數據,需要等待網絡上的數據分組到達,然後被複制到內核的緩衝區。

第二階段:將數據從內核緩衝區拷貝到用戶空間。
例如:recv() 接收連接發送的數據後,需要把數據複製到內核緩衝區,再從內核緩衝區複製到進程用戶空間。

也正因爲存在這兩個階段,Linux系統升級迭代中出現了五種網絡 IO 模型。
①阻塞 IO 模型 - Blocking IO
當應用進程調用了 recv() 這個系統調用,內核就開始了 IO 的第一個階段:準備數據。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩衝區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞。
當內核一直等到數據準備好了,它就會將數據從內核中拷貝到用戶內存,然後內核返回結果,用戶進程才解除阻塞的狀態。

②非阻塞 I/O 模型 - Non-Blocking IO(並不是說的Java的NIO,Java的NIO不是利用這個IO模型實現的)
當用戶進程發出recv()操作時,如果內核中的數據還沒有準備好,那麼它並不會阻塞用戶進程,而是立刻返回一個錯誤碼。
一旦內核中的數據準備好了,並且又再次收到了用戶進程的系統調用,那麼它馬上就將數據拷貝到了用戶內存,然後返回。

③I/O多路複用 - IO multiplexing(Linux 內核代碼迭代過程中,多路複用的網絡I/O模型依次支持了 SELECT(不停的輪詢)、POLL(不停的輪詢)、EPOLL(系統通知)三種。JavaNIO的實現是基於這個模型,也是常說的Reactor模式模式)
以 select 爲例,當用戶進程調用了 select,那麼整個進程會被阻塞,而此時,內核會監視所有 select 負責的 socket,
當任何一個 socket 中的數據準備好了,select 就會返回。這個時候用戶進程再調用 recv 操作,將數據從內核拷貝到用戶進程。
對應JavaNIO的實現就是,當調用socketchannel.read的時候其實是阻塞的,

④信號驅動 I/O - Signal driven IO
應用進程使用 sigaction 系統調用,預先告知內核,向內核註冊這樣一個函數,內核立即返回,應用進程可以繼續執行,也就是說等待數據階段應用進程是非阻塞的。
內核在數據到達時嚮應用進程發送 SIGIO 信號,應用進程收到之後在信號處理程序中調用 recv() 將數據從內核複製到應用進程中。

⑤異步 I/O 模型 - Asynchronous IO
應用進程執行 aio_read() 系統調用會立即返回,應用進程可以繼續執行,不會被阻塞,內核會在所有操作完成之後嚮應用進程發送信號。
異步IO模型,要求等待數據和數據拷貝操作的兩個處理階段上都不能等待(blocking),內核自行去準備好數據並將數據從內核緩衝區中複製到應用進程的緩衝區,
再通知應用進程讀操作完成了,然後應用進程再去處理。

遺憾的是,Linux 的網絡 IO 模型中是不存在異步IO的,AIO的底層實現仍使用EPOLL。Linux的網絡IO處理的第二階段總是阻塞等待數據copy完成的。

下面詳細說一下NIO,這裏的NIO不是說的上面的非阻塞I/O模型,而是JavaNIO,基於I/O多路複用模型實現的.
NIO的主要事件有幾個:讀就緒、寫就緒、有新連接到來。

我們首先需要註冊當這幾個事件到來的時候所對應的處理器。然後在合適的時機告訴事件選擇器:我對這個事件感興趣。對於寫操作,就是寫不出去的時候對寫事件感興趣;對於讀操作,就是完成連接和系統沒有辦法承載新讀入的數據的時;對於accept,一般是服務器剛啓動的時候;而對於connect,一般是connect失敗需要重連或者直接異步調用connect的時候。

其次,用一個死循環選擇就緒的事件,會執行系統調用(Linux2.6之前是select、poll,2.6之後是epoll,Windows是IOCP),還會阻塞的等待新事件的到來。新事件到來的時候,會在selector上註冊標記位,標示可讀、可寫或者有連接到來。

注意,select是阻塞的,無論是通過操作系統的通知(epoll)還是不停的輪詢(select,poll),這個函數是阻塞的。所以你可以放心大膽地在一個while(true)裏面調用這個函數而不用擔心CPU空轉。
所以我們的程序大概的模樣是:

interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//讀,寫或者連接
}

//IO線程主循環:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//選擇就緒的事件和對應的連接
if(channel.eventaccept){
registerNewChannelHandler(channel);//如果是新連接,則註冊一個新的讀寫處理器
}
if(channel.event
write){
getChannelHandler(channel).channelWritable(channel);//如果可以寫,則執行寫事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以讀,則執行讀事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的對應事件處理器
}

這個程序很簡短,也是最簡單的Reactor模式:註冊所有感興趣的事件處理器,單線程輪詢選擇就緒事件,執行事件處理器。

協程的實現.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章